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

@@ -3,26 +3,72 @@
"version": "0.0.1",
"description": "Coze Web ChatApp SDK ",
"license": "Apache-2.0",
"author": "gaoding.devingao@bytedance.com",
"author": "yangyu.1@bytedance.com",
"maintainers": [
"gaoding.devingao@bytedance.com"
"gaoding.devingao@bytedance.com",
"yangyu.1@bytedance.com"
],
"sideEffects": false,
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./types": "./src/exports/types.ts",
"./envs": "./src/util/env.ts"
},
"main": "src/index.ts",
"types": "./src/index.ts",
"typesVersions": {
"*": {
"types": [
"./src/exports/types.ts"
],
"envs": [
"./src/util/env.ts"
]
}
},
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
"test:cov": "vitest run --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@coze/chat-sdk": "0.1.11-beta.19",
"react": "~18.2.0"
"@coze-arch/idl": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-common/chat-answer-action": "workspace:*",
"@coze-common/chat-area": "workspace:*",
"@coze-common/chat-area-plugin-chat-background": "workspace:*",
"@coze-common/chat-area-plugin-message-grab": "workspace:*",
"@coze-common/chat-area-plugin-reasoning": "workspace:*",
"@coze-common/chat-area-plugins-chat-shortcuts": "workspace:*",
"@coze-common/chat-core": "workspace:*",
"@coze-common/chat-uikit": "workspace:*",
"@coze-common/chat-uikit-shared": "workspace:*",
"@coze-common/chat-workflow-render": "workspace:*",
"@coze-studio/file-kit": "workspace:*",
"@coze-studio/open-env-adapter": "workspace:*",
"@coze-studio/slardar-adapter": "workspace:*",
"@coze/api": "1.3.5",
"@douyinfe/semi-icons": "^2.36.0",
"ahooks": "^3.7.8",
"axios": "^1.4.0",
"classnames": "^2.3.2",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.7",
"eventemitter3": "^5.0.1",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"react": "~18.2.0",
"react-device-detect": "2.2.3",
"react-dom": "~18.2.0",
"react-router-dom": "^6.11.1",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-env": "workspace:*",
@@ -34,7 +80,6 @@
"@coze-arch/tailwind-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@coze-studio/open-env-adapter": "workspace:*",
"@rspack/plugin-react-refresh": "0.6.0",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="currentColor">
<path d="M13.4997 16.5H4.49881C2.84338 16.5 1.5 15.1574 1.5 13.5013V4.50088C1.5 2.84333 2.84338 1.5 4.49881 1.5H13.4997C15.158 1.5 16.5 2.84331 16.5 4.50088V13.5013C16.5 15.1574 15.158 16.5 13.4997 16.5ZM15.0013 5.25092C15.0013 4.00792 13.9919 3.00008 12.7496 3.00008H5.24889C4.00657 3.00008 2.99864 4.00792 2.99864 5.25092V12.7498C2.99864 13.9943 4.00657 15.0007 5.24889 15.0007H12.7496C13.9919 15.0007 15.0013 13.9943 15.0013 12.7498V5.25092ZM9.74933 11.9997C9.74933 12.414 9.41351 12.7498 8.99925 12.7498C8.585 12.7498 8.24918 12.414 8.24918 11.9997V9.75115H5.99967C5.58503 9.75115 5.24889 9.41502 5.24889 9.00037C5.24889 8.58572 5.58503 8.24958 5.99967 8.24958H8.24918V6.00099C8.24918 5.58674 8.585 5.25092 8.99925 5.25092C9.41351 5.25092 9.74933 5.58674 9.74933 6.001V8.24958H11.9988C12.4135 8.24958 12.7496 8.58572 12.7496 9.00037C12.7496 9.41502 12.4135 9.75115 11.9988 9.75115H9.74933V11.9997Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,23 @@
.container {
padding: 20px;
.label-value {
display: flex;
width: 100%;
font-size: 14px;
margin-bottom: 10px;
.label {
width: 150px;
}
.value {
flex: 1;
}
.img {
max-width: 200px;
height: auto;
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type PropsWithChildren } from 'react';
import { type IBuilderChatProps } from '../../type';
import styles from './index.module.less';
const LabelValue: FC<PropsWithChildren<{ label: string }>> = ({
label,
children,
}) => (
<div className={styles['label-value']}>
<div className={styles.label}>{label}:</div>
<div className={styles.value}>{children}</div>
</div>
);
export const AuditPanel: FC<IBuilderChatProps> = props => (
<div className={styles.container}>
<LabelValue label="Bot名称">{props?.project?.name}</LabelValue>
<LabelValue label="BotIcon">
<img src={props?.project?.iconUrl} className={styles.img} />
</LabelValue>
<LabelValue label="开场白">
{props?.project?.onBoarding?.prologue}
</LabelValue>
<LabelValue label="推荐词">
{(props?.project?.onBoarding?.suggestions || []).map((item, index) => (
<div key={`${index}`}>{item}</div>
))}
</LabelValue>
<LabelValue label="用户名称">{props?.userInfo?.nickname}</LabelValue>
<LabelValue label="用户头像">
<img src={props?.userInfo?.url} className={styles.img} />
</LabelValue>
<LabelValue label="输入框placholder">
{props?.areaUi?.input?.placeholder}{' '}
</LabelValue>
<LabelValue label="输入框默认值">
{props?.areaUi?.input?.defaultText}{' '}
</LabelValue>
</div>
);

View File

@@ -0,0 +1,53 @@
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
box-sizing: border-box;
.mask {
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 12%);
width: 100%;
height: 100%;
z-index: 3;
&::after {
content: '';
background: linear-gradient(
180deg,
rgba(99, 99, 99, 40%),
rgba(99, 99, 99, 0%)
);
height: 216px;
position: relative;
display: block;
width: 100%;
z-index: 10;
}
}
.img-container {
height: 100%;
width: 100%;
position: relative;
transform: translateX(-50%);
left: 50%;
z-index: 2;
overflow: hidden;
}
.img {
position: relative;
left: 50%;
width: 100%;
height: 100%;
transform: translateX(-50%);
object-fit: cover;
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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, useRef } from 'react';
import styles from './index.module.less';
export const Background: FC<{
bgInfo?: {
imgUrl: string;
themeColor: string; // 背景颜色
};
}> = props => {
const targetRef = useRef(null);
const { bgInfo } = props;
if (!bgInfo || !bgInfo?.imgUrl) {
return null;
}
const { themeColor = 'transparent', imgUrl } = bgInfo;
return (
<div
ref={targetRef}
className={styles['bg-image']}
style={{
backgroundColor: themeColor,
}}
>
<div className={styles.mask} />
<div className={styles['img-container']}>
{imgUrl ? <img src={imgUrl} className={styles.img} /> : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,74 @@
/*
* 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 {
createContext,
type FC,
type PropsWithChildren,
useContext,
} from 'react';
import { useUpdateEffect } from 'ahooks';
import { type IBuilderChatProps } from '../type';
import { combineAppDataWithProps } from '../services/get-bot-info';
import { type InitData } from '../data-type';
interface BuilderChatContextValue {
appDataFromOnLine?: InitData | null;
appDataCombineWithProps?: InitData | null;
}
type BuilderChatContextProps = BuilderChatContextValue & {
setAppDataFromOnLine?: (appDataFromOnLint: InitData | null) => void;
setAppDataCombineWithProps?: (
appDataCombineWithProps: InitData | null,
) => void;
};
const BuilderChatContext = createContext<BuilderChatContextProps>({
appDataFromOnLine: null,
appDataCombineWithProps: null,
});
export const BuilderChatProvider: FC<
PropsWithChildren<BuilderChatContextProps>
> = ({ children, ...props }) => (
<BuilderChatContext.Provider value={props} children={children} />
);
export const useGetAppDataFromOnLine = () => {
const { appDataFromOnLine } = useContext(BuilderChatContext);
return appDataFromOnLine;
};
export const useGetAppDataCombineWithProps = () => {
const { appDataCombineWithProps } = useContext(BuilderChatContext);
return appDataCombineWithProps;
};
export const useSetAppDataFromOnLine = () => {
const { setAppDataFromOnLine } = useContext(BuilderChatContext);
return setAppDataFromOnLine;
};
export const useUpdateAppDataCombineWithProps = (props: IBuilderChatProps) => {
const { appDataFromOnLine, setAppDataCombineWithProps } =
useContext(BuilderChatContext);
useUpdateEffect(() => {
if (appDataFromOnLine) {
const formatAPPInfo = combineAppDataWithProps(appDataFromOnLine, props);
setAppDataCombineWithProps?.(formatAPPInfo);
}
}, [appDataFromOnLine, props]);
};

View File

@@ -0,0 +1,341 @@
/*
* 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,
useImperativeHandle,
useMemo,
type FC,
forwardRef,
type Ref,
useRef,
type PropsWithChildren,
useState,
memo,
} from 'react';
import cls from 'classnames';
import { type InputController } from '@coze-common/chat-uikit-shared';
import { ContentType } from '@coze-common/chat-core/message/types';
import {
useSendTextMessage,
useSendNormalizedMessage,
} from '@coze-common/chat-area/hooks/messages/use-send-message';
import { useClearContext } from '@coze-common/chat-area/hooks/messages/use-clear-context';
import { useInitStatus } from '@coze-common/chat-area/hooks/context/use-init-status';
import { I18n } from '@coze-arch/i18n';
import type { StudioChatProviderProps } from '@/types/props';
import { Layout } from '@/types/client';
import { useGetTheme } from '@/components/studio-open-chat/hooks/use-get-theme';
import {
OpenChatProvider,
StudioChatArea,
} from '@/components/studio-open-chat';
import { Loading } from '@/components/loading';
import { ChatHeader } from '@/components/header';
import ChatFooter from '@/components/footer';
import ErrorFallback, {
type InitErrorFallback,
} from '@/components/error-fallback';
import { ErrorBoundary } from '@/components/error-boundary';
import {
type IBuilderChatProps,
type MessageType,
type BuilderChatRef,
} from './type';
import { getBuilderEventCallbackPlugin } from './plugins/event-callback';
import { useRequestInit } from './hooks/use-request-init';
import { useOnboardingUpdate } from './hooks/use-on-boarding-update';
import { useInitChat } from './hooks/use-init-chat';
import { useCoreManager } from './hooks/use-core-manager';
import { useBotAndUserUpdate } from './hooks/use-bot-user-update';
import { type InitData } from './data-type';
import {
BuilderChatProvider,
useGetAppDataCombineWithProps,
useUpdateAppDataCombineWithProps,
} from './context/builder-chat-context';
import { Background } from './components/background';
import { AuditPanel } from './components/audit-panel';
import styles from './index.module.less';
export { type BuilderChatRef };
export const BuilderChatContent = forwardRef(
(
{
uiBuilderProps,
chatProps,
}: {
chatProps: StudioChatProviderProps;
uiBuilderProps: IBuilderChatProps;
},
ref: Ref<BuilderChatRef>,
) => {
const refHasInitController = useRef(false);
const refInputController = useRef<InputController>();
const { areaUi } = uiBuilderProps;
const handleClearContext = useClearContext();
const sendMessage = useSendNormalizedMessage();
const sendTextMessage = useSendTextMessage();
useOnboardingUpdate();
useBotAndUserUpdate();
useImperativeHandle(
ref,
() => ({
sendMessage: (message: MessageType) => {
if (message.type === ContentType.Text) {
sendTextMessage({ text: message.text, mentionList: [] }, 'other');
} else if (message.type === ContentType.Image) {
sendMessage(
{
payload: {
contentType: ContentType.Image,
contentObj: {
image_list: [message.value],
},
mention_list: [],
},
},
'other',
);
} else if (message.type === 'file') {
sendMessage(
{
payload: {
contentType: ContentType.File,
contentObj: {
file_list: [message.value],
},
mention_list: [],
},
},
'other',
);
}
},
clearContext: () => {
handleClearContext?.();
},
}),
[handleClearContext, sendTextMessage, sendMessage],
);
useEffect(() => {
refInputController.current?.setInputText?.(
areaUi?.input?.defaultText || '',
);
}, [areaUi?.input?.defaultText]);
const renderChatInputTopSlot = areaUi?.input?.renderChatInputTopSlot
? () => areaUi?.input?.renderChatInputTopSlot?.(false)
: undefined;
const isMobile = uiBuilderProps.project?.layout === Layout.MOBILE;
const theme = useGetTheme();
const { header } = uiBuilderProps.areaUi || {};
return (
<StudioChatArea
{...chatProps}
{...(areaUi || {})}
coreAreaClassName={styles['core-area']}
inputPlaceholder={
areaUi?.input?.placeholder || I18n.t('chatInputPlaceholder')
}
messageMaxWidth={
uiBuilderProps?.project?.mode !== 'websdk' ? '600px' : undefined
}
enableMultimodalUpload={true}
showInputArea={areaUi?.input?.isShow}
messageGroupListClassName={styles['scroll-view']}
renderChatInputTopSlot={renderChatInputTopSlot}
isShowClearContextDivider={true}
headerNode={
<ChatHeader
title={header?.title || ''}
iconUrl={header?.icon}
extra={header?.extra}
theme={theme}
isMobile={isMobile}
isShowConversations={false} // app 这期不支持
isShowHeader={header?.isShow}
/>
}
isMiniScreen={areaUi?.uiTheme === 'chatFlow' ? true : false}
inputNativeCallbacks={{
getController: inputControllerIn => {
refInputController.current = inputControllerIn;
if (!refHasInitController.current) {
refInputController.current?.setInputText?.(
areaUi?.input?.defaultText || '',
);
refHasInitController.current = true;
}
},
}}
/>
);
},
);
const getErrorCallbackComp =
(props: IBuilderChatProps & { refresh: () => void }): FC<InitErrorFallback> =>
({ error, onBeforeRetry }) => (
<>
<ErrorFallback
error={error}
onBeforeRetry={onBeforeRetry}
refresh={props.refresh}
/>
{props.areaUi?.input?.renderChatInputTopSlot?.(true)}
</>
);
const BuilderChatWrap: FC<PropsWithChildren<IBuilderChatProps>> = ({
children,
...props
}) => {
const initStatus = useInitStatus();
const theme = useGetTheme();
const { footer } = props.areaUi || {};
const isMobile = props.project?.layout === Layout.MOBILE;
const appInfoResult = useGetAppDataCombineWithProps();
const footerConfig = {
...(footer || {
expressionText: '',
}),
};
if (props.project?.mode !== 'websdk') {
if (!footerConfig.expressionText) {
footerConfig.expressionText = I18n.t('chat_GenAI_tips');
}
}
if (initStatus !== 'initSuccess') {
return props?.areaUi?.renderLoading?.() || <Loading />;
}
return (
<div
className={cls(styles.content, {
[styles.mobile]: isMobile,
[styles['bg-theme']]: theme === 'bg-theme',
})}
style={props.style}
>
<div
className={cls(styles.area, {
[styles['chat-flow-area']]: props.areaUi?.uiTheme === 'chatFlow',
[styles['chat-ui-builder']]: props.areaUi?.uiTheme === 'uiBuilder',
})}
>
<Background bgInfo={appInfoResult?.customBgInfo} />
{children}
</div>
<ChatFooter {...footerConfig} theme={theme} />
</div>
);
};
const BuilderChatContainer = memo(
forwardRef((props: IBuilderChatProps, ref: Ref<BuilderChatRef>) => {
const { chatProps, hasReady, error, refresh } = useInitChat(props);
const openRequestInit = useRequestInit(props);
const builderEventCallbackPlugin = getBuilderEventCallbackPlugin({
eventCallbacks: props.eventCallbacks,
});
const appInfoResult = useGetAppDataCombineWithProps();
useUpdateAppDataCombineWithProps(props);
const plugins = [builderEventCallbackPlugin];
const requestManagerOptions = useCoreManager(props);
const userInfo = useMemo(
() => ({
url: '',
nickname: '',
...(props.userInfo || {}),
id: props?.userInfo?.id || chatProps?.userInfo?.id || '',
}),
[props?.userInfo, chatProps],
);
const ErrorFallbackComp = getErrorCallbackComp({ ...props, refresh });
if (props?.project?.mode === 'audit') {
return <AuditPanel {...props} />;
}
if (error) {
return <ErrorFallbackComp error={null} refresh={refresh} />;
}
if (!chatProps || !hasReady) {
return props?.areaUi?.renderLoading?.() || <Loading />;
}
const isCustomBackground = !!appInfoResult?.customBgInfo?.imgUrl || false;
console.log(
'[result] isCustomBackground:',
isCustomBackground,
appInfoResult?.customBgInfo,
);
return (
<OpenChatProvider
{...chatProps}
userInfo={userInfo}
openRequestInit={openRequestInit}
plugins={plugins}
requestManagerOptions={requestManagerOptions}
initErrorFallbackFC={ErrorFallbackComp}
onImageClick={props.eventCallbacks?.onImageClick}
debug={props.debug}
isCustomBackground={isCustomBackground}
onThemeChange={props?.eventCallbacks?.onThemeChange}
readonly={props?.areaUi?.isDisabled}
spaceId={props?.spaceId}
>
<BuilderChatWrap {...props}>
<BuilderChatContent
ref={ref}
uiBuilderProps={props}
chatProps={chatProps}
/>
</BuilderChatWrap>
</OpenChatProvider>
);
}),
);
export const BuilderChatWeb = forwardRef(
(props: IBuilderChatProps, ref: Ref<BuilderChatRef>) => {
const [appDataFromOnLine, setAppDataFromOnLine] = useState<InitData | null>(
null,
);
const [appDataCombineWithProps, setAppDataCombineWithProps] =
useState<InitData | null>(null);
return (
<ErrorBoundary>
<BuilderChatProvider
{...{
appDataFromOnLine,
setAppDataFromOnLine,
appDataCombineWithProps,
setAppDataCombineWithProps,
}}
>
<BuilderChatContainer ref={ref} {...props} />
</BuilderChatProvider>
</ErrorBoundary>
);
},
);

View File

@@ -0,0 +1,116 @@
/*
* 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 MixInitResponse } from '@coze-common/chat-area';
import { type EInputMode } from '@/types/props';
export interface ProjectInfoResp {
data: {
icon_url: string;
name: string;
};
}
export interface SuggestPromoteInfo {
suggestReplyMode?: number;
customizedSuggestPrompt?: string;
}
export interface BackgroundImageResp {
theme_color?: string;
gradient_position: {
left?: number;
right?: number;
};
canvas_position: {
width?: number;
height?: number;
left?: number;
top?: number;
};
image_url?: string;
origin_image_url?: string;
}
export interface WorkflowInfoResp {
role?: {
avatar?: {
image_uri?: string;
image_url?: string;
};
description?: string;
name?: string;
background_image_info?: {
web_background_image?: BackgroundImageResp;
mobile_background_image: BackgroundImageResp;
};
id: string;
connector_id: string;
suggest_reply_info?: {
suggest_reply_mode?: number;
customized_suggest_prompt?: string;
};
audio_config?: {
is_text_to_voice_enable?: boolean;
voice_config_map?: Record<
string,
{
voice_id: string;
name: string;
}
>;
};
workflow_id: string;
onboarding_info: {
prologue: string;
display_all_suggestions: boolean;
suggested_questions: string[];
};
user_input_config?: {
default_input_mode: number;
};
};
}
export type InitData = Pick<
MixInitResponse,
'prologue' | 'onboardingSuggestions' | 'backgroundInfo'
> & {
prologue: string;
onboardingSuggestions: {
id: string;
content: string;
}[];
botInfo: {
url: string;
nickname: string;
id: string;
};
displayAllSuggest?: boolean;
suggestPromoteInfo?: SuggestPromoteInfo;
defaultInputMode?: EInputMode;
customBgInfo?: {
imgUrl: string;
themeColor: string; // 背景颜色
};
};
export interface CozeApiFullFilledRes {
status: string;
reason: {
code: number;
};
data: unknown;
}

View File

@@ -0,0 +1,32 @@
/*
* 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 { webSdkDefaultConnectorId, chatflowDraftConnectorId } from '@/util';
import { type IBuilderChatProps } from '../type';
export const getConnectorId = (props: IBuilderChatProps) => {
const { project } = props;
const { mode, connectorId } = project || {};
if (!connectorId) {
if (mode === 'websdk') {
return webSdkDefaultConnectorId;
} else if (mode === 'draft') {
return chatflowDraftConnectorId;
}
}
return connectorId;
};

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useUpdateEffect } from 'ahooks';
import { useBotInfo, useChatArea } from '@coze-common/chat-area';
import { useGetAppDataCombineWithProps } from '../context/builder-chat-context';
// conversationId、sectionId 重新修改
export const useBotAndUserUpdate = () => {
const { updateBotInfo } = useBotInfo();
const { recordBotInfo } = useChatArea();
const appInfoResult = useGetAppDataCombineWithProps();
useUpdateEffect(() => {
const id = appInfoResult?.botInfo?.id || '';
recordBotInfo({
name: appInfoResult?.botInfo?.nickname || '',
avatar: appInfoResult?.botInfo?.url || '',
});
updateBotInfo(() => ({
[id]: {
url: appInfoResult?.botInfo?.url || '',
nickname: appInfoResult?.botInfo?.nickname || '',
id: appInfoResult?.botInfo?.id || '',
allowMention: false,
},
}));
}, [
appInfoResult?.botInfo?.nickname,
appInfoResult?.botInfo?.url,
appInfoResult?.botInfo?.id,
]);
};

View File

@@ -0,0 +1,131 @@
/*
* 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 { useMemo, useRef } from 'react';
import {
RequestScene,
type RequestManagerOptions,
} from '@coze-common/chat-core';
import { openApiHostByRegionWithToken } from '@/util/env';
import { type IBuilderChatProps } from '../type';
import { getConnectorId } from '../helper/get-connector-id';
import { useGetAppDataCombineWithProps } from '../context/builder-chat-context';
export const useCoreManager = (
props: IBuilderChatProps,
): RequestManagerOptions => {
const refProps = useRef(props);
const appData = useGetAppDataCombineWithProps();
const refAppData = useRef(appData);
refProps.current = props;
refAppData.current = appData;
return useMemo(
() => ({
scenes: {
[RequestScene.SendMessage]: {
hooks: {
onBeforeSendMessage: [
requestConfig => {
const { body } = requestConfig;
const bodyDataOld = JSON.parse(body);
const bodyData: Record<string, unknown> = {};
bodyData.additional_messages =
bodyDataOld.additional_messages || [];
bodyData.connector_id = bodyDataOld.connector_id;
bodyData.workflow_id = refProps?.current?.workflow?.id;
bodyData.parameters = refProps?.current?.workflow?.parameters;
bodyData.version =
refProps?.current?.project?.version || undefined;
bodyData.execute_mode =
refProps?.current?.project?.mode === 'draft'
? 'DEBUG'
: undefined;
bodyData.app_id =
refProps?.current?.project?.type === 'app'
? refProps?.current?.project?.id
: undefined;
bodyData.bot_id =
refProps?.current?.project?.type === 'bot'
? refProps?.current?.project?.id
: undefined;
bodyData.conversation_id = new URL(
requestConfig.url,
).searchParams.get('conversation_id');
bodyData.connector_id = getConnectorId(refProps?.current);
bodyData.ext = {
_caller: refProps?.current?.project?.caller,
user_id: bodyDataOld.user_id,
};
bodyData.suggest_reply_info = refAppData.current
?.suggestPromoteInfo
? {
suggest_reply_mode:
refAppData.current?.suggestPromoteInfo
?.suggestReplyMode,
customized_suggest_prompt:
refAppData.current?.suggestPromoteInfo
?.customizedSuggestPrompt,
}
: undefined;
requestConfig.body = JSON.stringify(bodyData);
requestConfig.url = `${openApiHostByRegionWithToken}/v1/workflows/chat`;
requestConfig.headers.push(
...Object.entries(refProps.current?.workflow?.header || {}),
);
return {
...requestConfig,
};
},
],
},
},
[RequestScene.ClearHistory]: {
hooks: {
onBeforeRequest: [
requestConfig => {
if (props?.project?.type === 'bot') {
requestConfig.data = {
connector_id: getConnectorId(props),
};
} else {
requestConfig.data = {
app_id: refProps?.current.project?.id,
conversation_name:
refProps?.current?.project?.conversationName,
get_or_create: false,
workflow_id: refProps?.current?.workflow?.id,
draft_mode: refProps?.current?.project?.mode === 'draft',
connector_id: getConnectorId(props),
};
}
return {
...requestConfig,
};
},
],
},
},
},
}),
[],
);
};

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { nanoid } from 'nanoid';
import { useUpdateEffect } from 'ahooks';
import { patPermissionApi } from '@coze-arch/bot-api';
import { type StudioChatProviderProps } from '@/types/props';
import { OpenApiSource } from '@/types/open';
import { AuthType } from '@/types/client';
import { type IBuilderChatProps } from '../type';
import { getConnectorId } from '../helper/get-connector-id';
const getToken = async () => {
try {
const res = await patPermissionApi.ImpersonateCozeUser({});
return res.data?.access_token ?? '';
} catch (_err) {
return '';
}
};
const checkParam = (props: IBuilderChatProps) => {
let error: Error | undefined;
if (props?.project?.type === 'bot') {
if (props?.project?.mode !== 'draft') {
error = new Error('mode must be draft when project type is bot');
}
} else {
if (props?.auth?.type !== 'internal') {
if (!props?.auth?.token) {
error = new Error('token is required when auth type is not internal');
}
}
}
return error;
};
// botId 、 token等修改
export const useInitChat = (
props: IBuilderChatProps,
): {
chatProps?: StudioChatProviderProps;
hasReady: boolean;
error: Error | null;
refresh: () => void;
} => {
const [hasReady, setHasReady] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [chatProps, setChatProps] = useState<StudioChatProviderProps>();
const { project: projectInfo, userInfo } = props;
useEffect(() => {
if (error || hasReady) {
return;
}
const errorTemp = checkParam(props);
if (errorTemp) {
setError(errorTemp);
return;
}
(async () => {
let token = props.auth?.token;
let refreshToken = props.auth?.refreshToken;
if (props.auth?.type === 'internal') {
token = await getToken();
refreshToken = getToken;
}
if (token) {
setChatProps({
chatConfig: {
bot_id: projectInfo.id,
auth: {
type: AuthType.TOKEN,
token,
onRefreshToken: refreshToken,
connectorId: getConnectorId(props),
},
ui: {
base: {
layout: projectInfo.layout,
},
chatBot: {
uploadable: props.areaUi.uploadable,
isNeedClearContext: props.areaUi.isNeedClearContext,
isNeedClearMessage: props.areaUi.isNeedClearMessage,
isNeedAddNewConversation:
props.areaUi.isNeedAddNewConversation ?? false, // 默认false
isNeedAudio: props.areaUi.input?.isNeedAudio ?? !IS_OVERSEA,
isNeedQuote: props.areaUi.isNeedQuote ?? false, // 默认false
isNeedFunctionCallMessage:
props.areaUi.isNeedFunctionCallMessage,
feedback: props.areaUi.feedback,
},
},
conversation_id: '', // 无用,可先为空
source: OpenApiSource.ChatFlow,
},
layout: projectInfo.layout,
userInfo: {
id: nanoid(),
url: '',
nickname: '',
...(userInfo || {}),
},
});
setHasReady(true);
} else {
setError(new Error('token is empty'));
}
})();
}, [error, hasReady]);
useUpdateEffect(() => {
setHasReady(false);
setError(null);
}, [
projectInfo?.id,
projectInfo?.type,
projectInfo?.conversationName,
projectInfo?.conversationId,
projectInfo?.mode,
projectInfo?.conversationId,
props?.workflow?.id,
]);
if (chatProps) {
chatProps.chatConfig.ui = chatProps.chatConfig.ui || {};
chatProps.chatConfig.ui.chatBot = chatProps.chatConfig.ui.chatBot || {};
chatProps.chatConfig.ui.chatBot.isNeedClearMessage =
props?.areaUi?.isNeedClearMessage;
chatProps.chatConfig.ui.chatBot.uploadable = props.areaUi?.uploadable;
chatProps.chatConfig.ui.chatBot.feedback = props.areaUi?.feedback;
}
return {
hasReady,
chatProps,
error,
refresh: () => {
setError(null);
setHasReady(false);
},
};
};

View File

@@ -0,0 +1,33 @@
/*
* 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 { useUpdateEffect } from 'ahooks';
import { useChatArea } from '@coze-common/chat-area';
import { useGetAppDataCombineWithProps } from '../context/builder-chat-context';
// conversationId、sectionId 重新修改
export const useOnboardingUpdate = () => {
const { partialUpdateOnboardingData } = useChatArea();
const appInfoResult = useGetAppDataCombineWithProps();
useUpdateEffect(() => {
partialUpdateOnboardingData(
appInfoResult?.prologue,
appInfoResult?.onboardingSuggestions,
);
}, [appInfoResult?.prologue, appInfoResult?.onboardingSuggestions]);
};

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 { useCallback, useRef } from 'react';
import { type CozeAPI } from '@coze/api';
import { type OpenRequestInit } from '@/types/props';
import { type IBuilderChatProps } from '../type';
import { combineAppDataWithProps, getBotInfo } from '../services/get-bot-info';
import { createOrGetConversation } from '../services/create-conversation';
import { useSetAppDataFromOnLine } from '../context/builder-chat-context';
// conversationId、sectionId 重新修改
export const useRequestInit = (props: IBuilderChatProps) => {
const refProps = useRef(props);
refProps.current = props;
const setAppDataFromOnLine = useSetAppDataFromOnLine();
const openRequestInit = useCallback(
async (apiSdk?: CozeAPI): Promise<OpenRequestInit> => {
const getBotInfoPrm = getBotInfo(apiSdk || undefined, refProps.current);
const createOrGetConversationPrm = createOrGetConversation(
apiSdk || undefined,
refProps.current,
);
const botInfo = await getBotInfoPrm;
const conversationInfo = await createOrGetConversationPrm;
setAppDataFromOnLine?.(botInfo || null);
const formatAPPInfo = combineAppDataWithProps(botInfo, refProps.current);
return {
...formatAPPInfo,
...conversationInfo,
isCustomBackground: !!formatAPPInfo.customBgInfo?.imgUrl,
isBuilderChat: true,
};
},
[],
);
return openRequestInit;
};

View File

@@ -0,0 +1,97 @@
/* stylelint-disable selector-class-pattern */
.content {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
:global {
.semi-upload-hidden-input,
.semi-upload-hidden-input-replace {
display: none;
}
:is(.-translate-x-1\/2) {
--tw-translate-x: -50%;
}
:is(.-translate-y-1\/2) {
--tw-translate-y: -50%;
}
}
.area {
flex: 1;
overflow: hidden;
.loading-wrap {
height: 100%;
width: 100%;
}
&.chat-flow-area {
margin: 0 -12px;
.loading-wrap {
margin: 0 12px;
}
}
}
.scroll-view {
padding-left: 0;
padding-right: 0;
}
:global(.chat-uikit-card-content) {
min-width: auto;
}
.footer {
background: none;
position: absolute;
width: 100%;
bottom: 2px;
.footer-text {
color: rgba(48, 59, 94, 47%);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
&.bg-theme {
.footer {
.footer-text {
color: rgba(255, 255, 255, 60%);
}
}
}
&.mobile {
.scroll-view {
padding: 0 12px;
}
.footer {
position: relative;
}
.chat-ui-builder {
.core-area {
width: 100%;
}
}
}
}
.chat-slot {
padding: 0 30px;
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//import { TaroBuilderChat } from './taro-chat';
import { BuilderChatWeb } from './coze-chat';
export type {
BuilderChatRef,
IWorkflow,
IProject,
IBuilderChatProps,
} from './type';
export const BuilderChat = BuilderChatWeb;

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 PluginRegistryEntry } from '@coze-common/chat-area';
import { type PluginBizContext } from './types/biz-context';
import { BizPlugin } from './plugin';
export type UIBuilderEventCallbackPlugin =
PluginRegistryEntry<PluginBizContext>;
export const getBuilderEventCallbackPlugin = (
props: PluginBizContext,
): PluginRegistryEntry<unknown> => {
const uiBuilderEventCallbackPlugin: UIBuilderEventCallbackPlugin = {
/**
* 贯穿插件生命周期、组件的上下文
*/
createPluginBizContext() {
return { ...props };
},
/**
* 插件本体
*/
Plugin: BizPlugin,
};
return uiBuilderEventCallbackPlugin as PluginRegistryEntry<unknown>;
};

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 {
PluginMode,
PluginName,
WriteableChatAreaPlugin,
createWriteableLifeCycleServices,
} from '@coze-common/chat-area';
import { type PluginBizContext } from './types/biz-context';
import { bizLifeCycleServiceGenerator } from './services/life-cycle';
export class BizPlugin extends WriteableChatAreaPlugin<PluginBizContext> {
/**
* 插件类型
* PluginMode.Readonly = 只读模式
* PluginMode.Writeable = 可写模式
*/
public pluginMode = PluginMode.Writeable;
/**
* 插件名称
* 请点 PluginName 里面去定义
*/
public pluginName = PluginName.UIBuilderEventcallbackPlugin;
/**
* 生命周期服务
*/
public lifeCycleServices = createWriteableLifeCycleServices(
this,
bizLifeCycleServiceGenerator,
);
}

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 WriteableAppLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
export const appLifeCycleServiceGenerator: WriteableAppLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({});

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 WriteableCommandLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
export const commandLifeCycleServiceGenerator: WriteableCommandLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({});

View File

@@ -0,0 +1,32 @@
/*
* 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 WriteableLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
import { renderLifeCycleServiceGenerator } from './render';
import { messageLifeCycleServiceGenerator } from './message';
import { commandLifeCycleServiceGenerator } from './command';
import { appLifeCycleServiceGenerator } from './app';
export const bizLifeCycleServiceGenerator: WriteableLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({
appLifeCycleService: appLifeCycleServiceGenerator(plugin),
messageLifeCycleService: messageLifeCycleServiceGenerator(plugin),
commandLifeCycleService: commandLifeCycleServiceGenerator(plugin),
renderLifeCycleService: renderLifeCycleServiceGenerator(plugin),
});

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 WriteableMessageLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
export const messageLifeCycleServiceGenerator: WriteableMessageLifeCycleServiceGenerator<
PluginBizContext
> = plugin => {
let hasInit = false;
let lastMessageId = '';
let nowReceivingReplyId = '';
return {
onBeforeMessageGroupListUpdate: ctx => {
const { messages } =
plugin.chatAreaPluginContext.readonlyAPI.message.getMessagesStoreInstantValues();
const latestMessage = messages?.[0];
if (!hasInit || lastMessageId === latestMessage?.message_id) {
hasInit = true;
return ctx;
}
plugin.pluginBizContext.eventCallbacks?.onMessageChanged?.();
lastMessageId = latestMessage?.message_id;
if (
(latestMessage?.type === 'answer' &&
latestMessage?.is_finish === true) ||
!lastMessageId
) {
plugin.pluginBizContext?.eventCallbacks?.onMessageReceivedFinish?.();
}
return ctx;
},
onAfterSendMessage: ctx => {
const chatflowExecuteId: string =
// @ts-expect-error -- linter-disable-autofix, 新添加参数,接口未支持
ctx?.message?.extra_info?.chatflow_execute_id || '';
if (chatflowExecuteId) {
plugin.pluginBizContext?.eventCallbacks?.onGetChatFlowExecuteId?.(
chatflowExecuteId,
);
}
plugin.pluginBizContext?.eventCallbacks?.onMessageSended?.();
},
onBeforeReceiveMessage: ctx => {
if (nowReceivingReplyId === ctx.message.reply_id) {
return;
}
nowReceivingReplyId = ctx.message.reply_id;
plugin.pluginBizContext?.eventCallbacks?.onMessageReceivedStart?.();
},
};
};

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 WriteableRenderLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
export const renderLifeCycleServiceGenerator: WriteableRenderLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({});

View File

@@ -0,0 +1,20 @@
/*
* 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 } from '../../../type';
export interface PluginBizContext {
eventCallbacks?: IEventCallbacks;
}

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' />

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 i18n from '@coze-arch/i18n/intl';
import { type CozeAPI } from '@coze/api';
import { type IBuilderChatProps } from '../type';
import { getConnectorId } from '../helper/get-connector-id';
export const createOrGetConversation = async (
apiSdk: CozeAPI | undefined,
props: IBuilderChatProps,
) => {
let conversationId = '';
let sectionId = '';
try {
if (props?.project?.type === 'bot') {
const res = await apiSdk?.conversations.create(
{
// @ts-expect-error -- linter-disable-autofix
connector_id: getConnectorId(props),
},
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
);
conversationId = res?.id || '';
// @ts-expect-error -- linter-disable-autofix
sectionId = res.last_section_id;
} else {
if (IS_OPEN_SOURCE) {
const res = (await apiSdk?.post(
'/v1/workflow/conversation/create',
{
app_id: props.project?.id,
conversation_name: props?.project?.conversationName,
get_or_create: true,
draft_mode: props?.project?.mode === 'draft',
workflow_id: props?.workflow?.id,
connector_id: getConnectorId(props),
},
false,
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
)) as {
data: {
id: string;
last_section_id: string;
};
};
conversationId = res?.data?.id || '';
sectionId = res?.data?.last_section_id || '';
} else {
const res = await apiSdk?.conversations.create(
{
// @ts-expect-error -- linter-disable-autofix
app_id: props.project?.id,
conversation_name: props?.project?.conversationName,
get_or_create: true,
draft_mode: props?.project?.mode === 'draft',
workflow_id: props?.workflow?.id,
connector_id: getConnectorId(props),
},
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
);
conversationId = res?.id || '';
sectionId = res?.last_section_id || '';
}
}
return { conversationId, sectionId };
} catch (error) {
throw {
code: -1002,
message: '',
};
}
};

View File

@@ -0,0 +1,188 @@
/*
* 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 { cloneDeep } from 'lodash-es';
import i18n from '@coze-arch/i18n/intl';
import { type CozeAPI } from '@coze/api';
import { EInputMode } from '@/types/props';
import ChatFlowUserIcon from '@/assets/chatflow-logo.png';
import { type IBuilderChatProps } from '../type';
import { getConnectorId } from '../helper/get-connector-id';
import {
type ProjectInfoResp,
type WorkflowInfoResp,
type InitData,
type CozeApiFullFilledRes,
} from '../data-type';
const getFormatAppData = (
appData?: ProjectInfoResp['data'],
workflowData?: WorkflowInfoResp['role'],
props?: IBuilderChatProps,
): InitData => {
const appInfoResult: InitData = {
prologue: workflowData?.onboarding_info?.prologue || '',
onboardingSuggestions:
workflowData?.onboarding_info?.suggested_questions?.map(
(item, index) => ({
id: index.toString(),
content: item,
}),
) || [],
displayAllSuggest: workflowData?.onboarding_info?.display_all_suggestions,
botInfo: {
url: workflowData?.avatar?.image_url || appData?.icon_url || '',
nickname:
workflowData?.name || appData?.name || props?.project.defaultName || '',
id: props?.project?.id || '',
},
suggestPromoteInfo: {
suggestReplyMode: workflowData?.suggest_reply_info?.suggest_reply_mode,
customizedSuggestPrompt:
workflowData?.suggest_reply_info?.customized_suggest_prompt,
},
backgroundInfo: workflowData?.background_image_info,
defaultInputMode:
workflowData?.user_input_config?.default_input_mode === 2
? EInputMode.Voice
: EInputMode.Text,
};
// 内部插件中用的是origin_image_url但是这里origin_image_url 会过期因此使用image_url重写
if (appInfoResult.backgroundInfo?.web_background_image) {
appInfoResult.backgroundInfo.web_background_image.origin_image_url =
appInfoResult.backgroundInfo.web_background_image.image_url;
}
if (appInfoResult.backgroundInfo?.mobile_background_image) {
appInfoResult.backgroundInfo.mobile_background_image.origin_image_url =
appInfoResult.backgroundInfo.mobile_background_image.image_url;
}
return appInfoResult;
};
export const combineAppDataWithProps = (
appInfoResultRaw: InitData,
props?: IBuilderChatProps,
): InitData => {
const appInfoResult = cloneDeep(appInfoResultRaw);
if (props?.project?.id) {
appInfoResult.botInfo.id = props?.project?.id;
}
if (props?.project?.name) {
appInfoResult.botInfo.nickname = props?.project?.name;
}
if (props?.project?.iconUrl) {
appInfoResult.botInfo.url = props?.project?.iconUrl;
}
if (props?.project?.onBoarding?.prologue) {
appInfoResult.prologue = props?.project?.onBoarding?.prologue;
}
if (props?.project?.onBoarding?.suggestions?.length) {
appInfoResult.onboardingSuggestions =
props?.project?.onBoarding?.suggestions.map((item, index) => ({
id: index.toString(),
content: item,
})) || [];
}
if (props?.project?.onBoarding?.displayAllSuggest) {
appInfoResult.displayAllSuggest =
props?.project?.onBoarding?.displayAllSuggest;
}
if (!appInfoResult.displayAllSuggest) {
appInfoResult.onboardingSuggestions =
appInfoResult.onboardingSuggestions.slice(0, 3);
}
if (props?.project?.suggestPromoteInfo?.suggestReplyMode) {
appInfoResult.suggestPromoteInfo = {
suggestReplyMode: props?.project?.suggestPromoteInfo?.suggestReplyMode,
customizedSuggestPrompt:
props?.project?.suggestPromoteInfo?.customizedSuggestPrompt,
};
}
if (props?.areaUi?.bgInfo?.imgUrl) {
// 去掉backgroundInfo 使用本地写的背景组件,不再使用插件进行背景显示。
appInfoResult.customBgInfo = props?.areaUi?.bgInfo;
} else {
appInfoResult.customBgInfo = undefined;
}
return appInfoResult;
};
export const getBotInfo = async (
apiSdk: CozeAPI | undefined,
props: IBuilderChatProps,
) => {
const connectorId = getConnectorId(props);
const isWebSdk = props?.project?.mode === 'websdk';
const workflowId = props?.workflow?.id;
const isDebugParam = props?.project?.mode === 'draft' ? 'true' : '';
const callerParam = props?.project?.caller || '';
const lang = i18n.language;
console.log('i18n.language', lang);
const [appRes, workflowRes] = await Promise.allSettled([
isWebSdk
? apiSdk?.get<unknown, ProjectInfoResp>(
`/v1/apps/${props?.project?.id}?version=${props?.project?.version || ''}&connector_id=${connectorId}`,
{},
false,
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
)
: null,
workflowId
? apiSdk?.get<unknown, { data: WorkflowInfoResp }>(
`/v1/workflows/${workflowId}?${[
`connector_id=${connectorId}`,
`is_debug=${isDebugParam}`,
`caller=${callerParam}`,
].join('&')}`,
{},
false,
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
)
: null,
]);
const appData =
appRes?.status === 'fulfilled' ? appRes?.value?.data : undefined;
const workflowData =
workflowRes?.status === 'fulfilled'
? workflowRes?.value?.data?.role
: undefined;
if (isWebSdk && !appData) {
throw { code: (appRes as CozeApiFullFilledRes)?.reason?.code, message: '' };
}
const appInfo = getFormatAppData(appData, workflowData, props);
if (props?.workflow?.id && !appInfo.botInfo.url) {
appInfo.botInfo.url = ChatFlowUserIcon;
}
return appInfo;
};

View File

@@ -0,0 +1,138 @@
/*
* 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 React from 'react';
import { type ImageModel, type FileModel } from '@coze-common/chat-core';
import { type ContentType } from '@coze-common/chat-area';
import { type OpenUserInfo } from '@/types/user';
import { type DebugProps } from '@/types/props';
import {
type Layout,
type FooterConfig,
type HeaderConfig,
type FeedbackConfig,
type ConversationsConfig,
} from '@/types/client';
import { type OnImageClick } from '@/types/';
export interface IWorkflow {
id?: string;
parameters?: Record<string, unknown>;
header?: Record<string, string>;
}
export interface SuggestPromoteInfo {
customizedSuggestPrompt?: string;
suggestReplyMode?: number;
}
export interface IProject {
id: string;
type: 'app' | 'bot';
mode: 'draft' | 'release' | 'websdk' | 'audit'; // 草稿模式 | 发布模式 | webSdk发布
caller?: 'UI_BUILDER' | 'CANVAS';
connectorId?: string;
conversationName?: string; // project的话必须填写
conversationId?: string; // type 为bot的话必须填写
sectionId?: string; // type 为bot的话必须填写
name?: string;
desc?: string;
defaultName?: string;
defaultIconUrl?: string;
iconUrl?: string;
layout?: Layout;
version?: string;
onBoarding?: {
prologue?: string;
displayAllSuggest?: boolean;
suggestions?: string[];
};
suggestPromoteInfo?: SuggestPromoteInfo;
}
export interface IEventCallbacks {
onMessageChanged?: () => void;
onMessageSended?: () => void;
onMessageReceivedStart?: () => void;
onMessageReceivedFinish?: () => void;
onImageClick?: OnImageClick;
onGetChatFlowExecuteId?: (id: string) => void;
onThemeChange?: (theme: 'bg-theme' | 'light') => void;
}
export interface IBuilderChatProps {
workflow: IWorkflow;
project: IProject;
spaceId?: string;
eventCallbacks?: IEventCallbacks;
userInfo?: OpenUserInfo;
areaUi: {
isDisabled?: boolean; // 默认 false
uploadable?: boolean; // 默认 true
isNeedClearContext?: boolean; // 是否显示 clearContext按钮
isNeedClearMessage?: boolean; // 是否显示 clearMessage按钮
isNeedAddNewConversation?: boolean; //是否显示新增会话
isNeedFunctionCallMessage?: boolean;
isNeedQuote?: boolean;
feedback?: FeedbackConfig;
input?: {
placeholder?: string;
renderChatInputTopSlot?: (isChatError?: boolean) => React.ReactNode;
isShow?: boolean; //默认 true
defaultText?: string;
isNeedAudio?: boolean; // 是否需要语音输入默认是false
isNeedTaskMessage?: boolean;
};
header?: HeaderConfig & {
title?: string;
icon?: string;
}; // 默认是
footer?: FooterConfig;
conversations?: ConversationsConfig;
uiTheme?: 'uiBuilder' | 'chatFlow'; // uiBuilder 的主题
renderLoading?: () => React.ReactNode;
bgInfo?: {
imgUrl: string;
themeColor: string; // 背景颜色
};
};
auth?: {
type: 'external' | 'internal'; // 内部: cookie换token 外部: internal
token?: string;
refreshToken?: () => Promise<string> | string;
};
style?: React.CSSProperties;
debug?: DebugProps;
}
export type MessageType =
| {
type: ContentType.Text;
text: string;
}
| {
type: ContentType.Image;
value: ImageModel;
}
| {
type: ContentType.File;
value: FileModel;
};
export interface BuilderChatRef {
sendMessage: (message: MessageType) => void;
clearContext: () => void;
}

View File

@@ -0,0 +1,19 @@
.coze-chat-app {
box-sizing: border-box;
display: flex;
flex-direction: column;
* {
box-sizing: border-box;
}
&.bordered {
border-radius: 8px;
box-shadow: 0 6px 8px 0 rgb(29 28 35 / 6%), 0 0 2px 0 rgb(29 28 35 / 18%);
}
.content {
flex: 1 1 auto;
min-height: 0;
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import cs from 'classnames';
import { useInitStatus } from '@coze-common/chat-area';
import { webSdkDefaultConnectorId } from '@/util';
import {
type StudioChatProviderProps,
type WebSdkChatProps,
} from '@/types/props';
import { OpenApiSource } from '@/types/open';
import { ChatType, Layout } from '@/types/client';
import { useGetTheme } from '@/components/studio-open-chat/hooks/use-get-theme';
import {
OpenChatProvider,
StudioChatArea,
} from '@/components/studio-open-chat';
import { Loading } from '@/components/loading';
import { ChatHeader } from '@/components/header';
import ChatFooter from '@/components/footer';
import ErrorFallback from '@/components/error-fallback';
import { ErrorBoundary } from '@/components/error-boundary';
import styles from './index.module.less';
export const WebSdkChat: FC<WebSdkChatProps> = ({
useInIframe = true,
...props
}) => {
const { chatConfig } = props ?? {};
if (!chatConfig?.bot_id) {
return null;
}
if (chatConfig.auth) {
chatConfig.auth.connectorId =
chatConfig.auth.connectorId || webSdkDefaultConnectorId;
}
return <CozeChat {...props} useInIframe={useInIframe} />;
};
const CozeChat: FC<WebSdkChatProps> = props => {
const {
layout,
className,
useInIframe = false,
chatConfig,
userInfo,
style,
onImageClick,
onThemeChange,
} = props;
if (!chatConfig.ui) {
chatConfig.ui = {};
}
if (!chatConfig.ui.chatBot) {
chatConfig.ui.chatBot = {};
}
if (chatConfig.auth?.type === 'token') {
chatConfig.ui.chatBot.isNeedClearMessage = false;
// chatConfig.ui.chatBot.isNeedAddNewConversation 不需要设置,按照用户的需要设置。
chatConfig.ui.chatBot.isNeedAddNewConversation =
chatConfig.ui.chatBot.isNeedAddNewConversation ?? true;
chatConfig.ui.chatBot.isNeedClearContext =
chatConfig.ui.chatBot.isNeedClearContext ?? true;
} else {
// 老版本的代码做兼容
chatConfig.ui.chatBot.isNeedClearMessage = true;
chatConfig.ui.chatBot.isNeedAddNewConversation = false;
chatConfig.ui.chatBot.isNeedClearContext = false;
}
const chatProps: StudioChatProviderProps = {
chatConfig: {
...chatConfig,
source: OpenApiSource.WebSdk,
},
layout,
userInfo,
initErrorFallbackFC: ErrorFallback,
onImageClick,
};
return (
<ErrorBoundary>
<div
className={cs(
styles.cozeChatApp,
!useInIframe && styles.bordered,
className,
)}
style={style}
>
<OpenChatProvider {...chatProps} onThemeChange={onThemeChange}>
<WebSdkChatArea chatProps={chatProps} webSdkProps={props} />
</OpenChatProvider>
</div>
</ErrorBoundary>
);
};
const WebSdkChatArea: FC<{
chatProps: StudioChatProviderProps;
webSdkProps: WebSdkChatProps;
}> = ({ chatProps, webSdkProps }) => {
const { layout, title, headerExtra, icon, chatConfig } = webSdkProps;
const initStatus = useInitStatus();
const isMobile = layout === Layout.MOBILE;
const { header: headerConf, conversations } = chatConfig?.ui || {};
const theme = useGetTheme();
const isShowConversations =
conversations?.isNeed && chatConfig.type !== ChatType.APP;
if (initStatus !== 'initSuccess') {
return <Loading />;
}
return (
<div className={styles.content}>
<StudioChatArea
{...chatProps}
enableMultimodalUpload={true}
headerNode={
<ChatHeader
title={title}
extra={headerExtra}
iconUrl={icon}
theme={theme}
isMobile={isMobile}
isShowConversations={isShowConversations}
isShowHeader={headerConf?.isShow !== false}
/>
}
/>
<ChatFooter {...(chatConfig?.ui?.footer || {})} theme={theme} />
</div>
);
};

View File

@@ -1,170 +0,0 @@
/*
* 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 '@coze/chat-sdk/webCss';
import { forwardRef, useMemo, type Ref, useImperativeHandle } from 'react';
import {
openApiHostByRegionWithToken,
openApiCdnUrlByRegion,
} from '@coze-studio/open-env-adapter';
import { I18n } from '@coze-arch/i18n';
import ChatSdk from '@coze/chat-sdk/webJs';
import {
type RawMessage,
type IChatFlowProps,
type Language,
} from '@coze/chat-sdk';
import { type IBuilderChatProps, type BuilderChatRef } from '@/types';
const {
ChatFlowFramework,
ChatSlot,
useSendMessage,
ChatType,
RawMessageType,
} = ChatSdk;
export { ChatType, RawMessageType };
export const ChatContent = forwardRef(
(_props: {}, ref: Ref<BuilderChatRef>) => {
const { sendMessage } = useSendMessage();
useImperativeHandle(
ref,
() => ({
sendMessage: (message: RawMessage) => {
sendMessage(message);
},
}),
[sendMessage],
);
return <ChatSlot />;
},
);
export const BuilderChat = forwardRef(
(props: IBuilderChatProps, ref: Ref<BuilderChatRef>) => {
const { workflow } = props;
const eventCallbacks: IChatFlowProps['eventCallbacks'] = useMemo(
() => ({
onImageClick: props.eventCallbacks?.onImageClick,
onGetChatFlowExecuteId: props.eventCallbacks?.onGetChatFlowExecuteId,
onThemeChange: props.eventCallbacks?.onThemeChange,
onInitSuccess: props.eventCallbacks?.onInitSuccess,
message: {
afterMessageReceivedFinish:
props.eventCallbacks?.afterMessageReceivedFinish,
},
}),
[props.eventCallbacks],
);
const { userInfo } = props;
const { auth } = props;
const areaUi: IChatFlowProps['areaUi'] = useMemo(
// eslint-disable-next-line complexity
() => ({
layout: props.project?.layout,
isDisabled: props.areaUi?.isDisabled,
input: {
isNeed: props.areaUi?.input?.isShow,
isNeedAudio: props.areaUi?.input?.isNeedAudio ?? !IS_OVERSEA,
placholder: props.areaUi?.input?.placeholder,
isNeedTaskMessage: props.areaUi?.input?.isNeedTaskMessage,
defaultText: props.areaUi?.input?.defaultText,
renderChatInputTopSlot: props.areaUi?.input?.renderChatInputTopSlot,
},
clearContext:
props.project?.mode === 'websdk'
? {
isNeed: true,
position: 'inputLeft',
}
: {
isNeed: false,
},
clearMessage:
props.project?.mode === 'websdk'
? {
isNeed: true,
position: 'headerRight',
}
: {
isNeed:
props.areaUi?.isNeedClearMessage !== undefined
? props.areaUi?.isNeedClearMessage
: true,
position: 'inputLeft',
},
uploadBtn: {
isNeed: props.areaUi?.uploadable,
},
uiTheme: props.areaUi?.uiTheme,
renderLoading: props.areaUi?.renderLoading,
header: {
isNeed: props.areaUi?.header?.isShow || false,
icon: props.project?.iconUrl,
title: props.project?.name,
renderRightSlot: () => <>{props.areaUi?.header?.extra || null}</>,
},
footer: props.areaUi?.footer,
}),
[props.areaUi, props.project],
);
const setting: IChatFlowProps['setting'] = useMemo(
() => ({
apiBaseUrl: openApiHostByRegionWithToken,
cdnBaseUrlPath: openApiCdnUrlByRegion,
language: I18n.language as Language,
logLevel: IS_BOE ? 'debug' : 'release',
...(props.setting || {}),
}),
[],
);
const project: IChatFlowProps['project'] = useMemo(
() => ({
id: props.project?.id || '',
type: props.project?.type,
mode: props.project?.mode as 'release',
caller: props.project?.caller,
defaultName: props.project?.defaultName,
defaultIconUrl: props.project?.defaultIconUrl,
connectorId: props.project?.connectorId,
conversationName: props.project?.conversationName,
name: props.project?.name,
iconUrl: props.project?.iconUrl,
OnBoarding: props.project?.onBoarding,
}),
[props.project],
);
return (
<>
<ChatFlowFramework
workflow={workflow}
project={project}
userInfo={userInfo}
eventCallbacks={eventCallbacks}
auth={auth}
style={props.style}
areaUi={areaUi}
setting={setting}
>
<ChatContent ref={ref} />
</ChatFlowFramework>
</>
);
},
);

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 { PcConversationItem } from './pc';
export { MobileConversationItem } from './mobile';

View File

@@ -0,0 +1,66 @@
.conversation-item {
display: flex;
flex-direction: column;
}
.conversation-item-time {
padding: 8px 8px 0;
color: var(--coz-fg-secondary);
font-size: 14px;
font-weight: 400;
line-height: 20px;
margin-top: 4px;
}
.conversation-item-content {
display: flex;
height: 40px;
padding: 4px 8px;
align-items: center;
align-self: stretch;
border-radius: var(--mini, 5px); /* 兼容 iOS/Safari/微信 */
user-select: none;
}
.conversation-item-content-active {
background: var(--coz-mg-secondary-hovered);
}
.conversation-item-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
// background: var(--coz-mg-mask);
// 半透明白色背景
background: rgba(255, 255, 255, 25%);
// 毛玻璃效果
backdrop-filter: blur(2.5px) saturate(1.2);
z-index: calc(var(--chat-z-index-input) + 2);
}
.conversation-item-content-touched {
animation: jelly 0.7s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
border: 1px solid var(--coz-stroke-primary);
}
.conversation-item-content-operate-visible {
z-index: calc(var(--chat-z-index-input) + 3);
background: var(--coz-bg-max);
border: 1px solid var(--coz-stroke-primary);
box-shadow: 0 0 8px 1px rgba(0, 0, 0, 20%);
}
@keyframes jelly {
0% { transform: scale(1); }
70% {
transform: scale(1.08); /* 慢慢放大到1.08 */
}
100% {
transform: scale(1);
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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 { useCallback, useRef, useState } from 'react';
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Typography } from '@coze-arch/coze-design';
import { type Conversation } from '@coze/api';
import {
conversationSortMap,
type SortedConversationItem,
} from '@/types/conversations';
import { MobileConversationOperate } from './operate';
import s from './index.module.less';
export const MobileConversationItem = ({
isActive,
item,
shouldDisplayTime,
onConversationChange,
onRename,
onDelete,
}: {
isActive: boolean;
item: SortedConversationItem;
shouldDisplayTime: boolean;
onConversationChange: (conversation: Conversation) => void;
onRename: (conversation: Conversation) => void;
onDelete: (conversation: Conversation) => void;
}) => {
const [visible, setVisible] = useState(false);
const [isTouched, setIsTouched] = useState(false);
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null);
const longClickTimerRef = useRef<NodeJS.Timeout | null>(null);
// 长按检测时间(毫秒)
const LONG_PRESS_DURATION = 500;
const LONG_CLICK_DURATION = 300;
const closeOperate = () => {
setVisible(false);
setIsTouched(false);
};
const handleTouchStart = useCallback((e: React.TouchEvent) => {
longPressTimerRef.current = setTimeout(() => {
setVisible(true);
}, LONG_PRESS_DURATION);
longClickTimerRef.current = setTimeout(() => {
setIsTouched(true);
}, LONG_CLICK_DURATION);
}, []);
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
if (longClickTimerRef.current) {
clearTimeout(longClickTimerRef.current);
longClickTimerRef.current = null;
}
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current);
longPressTimerRef.current = null;
}
}, []);
const handleTouchMove = useCallback(() => {
if (longClickTimerRef.current) {
clearTimeout(longClickTimerRef.current);
longClickTimerRef.current = null;
}
// 如果用户移动手指,取消长按
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current);
closeOperate();
longPressTimerRef.current = null;
}
}, []);
return (
<div className={s['conversation-item']}>
{shouldDisplayTime ? (
<div className={s['conversation-item-time']}>
{conversationSortMap.get(item.sort)}
</div>
) : null}
<MobileConversationOperate
onRename={() => {
onRename(item);
closeOperate();
}}
onDelete={() => {
onDelete(item);
closeOperate();
}}
visible={visible}
>
<div
className={cls(s['conversation-item-content'], {
[s['conversation-item-content-active']]: isActive,
[s['conversation-item-content-touched']]: isTouched,
[s['conversation-item-content-operate-visible']]: visible,
})}
onClick={() => {
if (visible) {
return;
}
onConversationChange(item);
}}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onTouchMove={handleTouchMove}
onContextMenu={e => {
e.preventDefault();
}}
>
<Typography.Text
style={{
flex: 1,
}}
ellipsis={{
showTooltip: {
opts: {
content: item.name,
style: {
wordBreak: 'break-all',
},
position: 'top',
spacing: 4,
},
},
}}
>
{item.name ||
I18n.t('web_sdk_conversation_default_name', {}, '新创建的会话')}
</Typography.Text>
</div>
</MobileConversationOperate>
{visible ? (
<div
onClick={e => {
e.stopPropagation();
e.preventDefault();
closeOperate();
}}
className={s['conversation-item-mask']}
></div>
) : null}
</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 { I18n } from '@coze-arch/i18n';
import { IconCozEdit, IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { Menu } from '@coze-arch/coze-design';
export const MobileConversationOperate = ({
onRename,
onDelete,
visible,
children,
}: {
onRename: () => void;
onDelete: () => void;
visible: boolean;
children: React.ReactNode;
}) => (
<>
<Menu
trigger="custom"
position="bottom"
visible={visible}
render={
<Menu.SubMenu mode="menu">
<Menu.Item
onClick={(_, e) => {
e.stopPropagation();
e.preventDefault();
onRename();
}}
icon={<IconCozEdit />}
>
{I18n.t('workflow_detail_node_rename', {}, '重命名')}
</Menu.Item>
<Menu.Item
onClick={(_, e) => {
e.stopPropagation();
e.preventDefault();
onDelete();
}}
icon={<IconCozTrashCan color="var(--coz-fg-hglt-red)" />}
>
<span style={{ color: 'var(--coz-fg-hglt-red)' }}>
{I18n.t('web_sdk_delete', {}, '删除')}
</span>
</Menu.Item>
</Menu.SubMenu>
}
>
{children}
</Menu>
</>
);

View File

@@ -0,0 +1,39 @@
.conversation-item {
display: flex;
flex-direction: column;
}
.conversation-item-time {
padding: 8px 8px 0;
color: var(--coz-fg-secondary);
font-size: 14px;
font-weight: 400;
line-height: 20px;
margin-top: 4px;
}
.conversation-item-content {
display: flex;
height: 40px;
padding: 4px 8px;
align-items: center;
align-self: stretch;
border-radius: var(--mini, 5px);
cursor: pointer;
.conversation-operate {
display: none;
}
&:hover {
background: var(--coz-mg-secondary-hovered);
.conversation-operate {
display: block;
}
}
}
.conversation-item-content-active {
background: var(--coz-mg-secondary-hovered);
}

View File

@@ -0,0 +1,113 @@
/*
* 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 cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozMore } from '@coze-arch/coze-design/icons';
import { IconButton, Typography } from '@coze-arch/coze-design';
import { type Conversation } from '@coze/api';
import {
conversationSortMap,
type SortedConversationItem,
} from '@/types/conversations';
import { Operate } from './operate';
import s from './index.module.less';
export const PcConversationItem = ({
isActive,
item,
shouldDisplayTime,
onConversationChange,
onRename,
onDelete,
}: {
isActive: boolean;
item: SortedConversationItem;
shouldDisplayTime: boolean;
onConversationChange: (conversation: Conversation) => void;
onRename: (conversation: Conversation) => void;
onDelete: (conversation: Conversation) => void;
}) => {
const [visible, setVisible] = useState(false);
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
setVisible(true);
};
return (
<div className={s['conversation-item']}>
{shouldDisplayTime ? (
<div className={s['conversation-item-time']}>
{conversationSortMap.get(item.sort)}
</div>
) : null}
<div
className={cls(s['conversation-item-content'], {
[s['conversation-item-content-active']]: isActive,
})}
onClick={() => onConversationChange(item)}
>
<Typography.Text
style={{
flex: 1,
}}
ellipsis={{
showTooltip: {
opts: {
content: item.name,
style: {
wordBreak: 'break-all',
},
position: 'top',
spacing: 4,
},
},
}}
>
{item.name ||
I18n.t('web_sdk_conversation_default_name', {}, '新创建的会话')}
</Typography.Text>
<Operate
onRename={() => {
onRename(item);
setVisible(false);
}}
onDelete={() => {
onDelete(item);
setVisible(false);
}}
visible={visible}
setVisible={setVisible}
>
<IconButton
className={s['conversation-operate']}
onClick={handleClick}
size="small"
icon={<IconCozMore />}
color="secondary"
/>
</Operate>
</div>
</div>
);
};

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { IconCozEdit, IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { Menu } from '@coze-arch/coze-design';
export const Operate = ({
children,
onRename,
onDelete,
visible,
setVisible,
}: {
children: React.ReactNode;
onRename: () => void;
onDelete: () => void;
visible: boolean;
setVisible: (visible: boolean) => void;
}) => (
<Menu
trigger="custom"
position="bottomLeft"
visible={visible}
onClickOutSide={() => setVisible(false)}
render={
<Menu.SubMenu mode="menu">
<Menu.Item
onClick={(_, e) => {
e.stopPropagation();
e.preventDefault();
onRename();
}}
icon={<IconCozEdit />}
>
{I18n.t('workflow_detail_node_rename', {}, '重命名')}
</Menu.Item>
<Menu.Item
onClick={(_, e) => {
e.stopPropagation();
e.preventDefault();
onDelete();
}}
icon={<IconCozTrashCan color="var(--coz-fg-hglt-red)" />}
>
<span style={{ color: 'var(--coz-fg-hglt-red)' }}>
{I18n.t('web_sdk_delete', {}, '删除')}
</span>
</Menu.Item>
</Menu.SubMenu>
}
>
{children}
</Menu>
);

View File

@@ -0,0 +1,40 @@
.conversations {
display: flex;
flex-direction: column;
min-width: 320px;
height: 100%;
}
.conversations-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
}
.conversations-header-title {
color: var(--coz-fg-primary);
font-size: 16px;
font-weight: 600;
line-height: 22px;
}
.conversations-create-button {
margin: 0 16px;
width: calc(100% - 32px);
}
.conversations-list {
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
flex: 1;
margin-bottom: 10px;
}
.conversations-list-group {
display: flex;
flex-direction: column;
padding: 0 8px;
}

View File

@@ -0,0 +1,296 @@
/*
* 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 {
useMemo,
useRef,
useState,
forwardRef,
useImperativeHandle,
useEffect,
} from 'react';
import { useShallow } from 'zustand/react/shallow';
import { I18n } from '@coze-arch/i18n';
import { IconCozPlus, IconCozSideNav } from '@coze-arch/coze-design/icons';
import { Button, IconButton, Spin, Toast } from '@coze-arch/coze-design';
import { type Conversation } from '@coze/api';
import {
type ConversationSort,
type SortedConversationItem,
} from '@/types/conversations';
import { Layout } from '@/types/client';
import {
PcConversationItem,
MobileConversationItem,
} from '../conversation-item';
import { type ChatState } from '../../studio-open-chat/store/store';
import { useChatAppProps, useChatAppStore } from '../../studio-open-chat/store';
import s from './index.module.less';
export interface ConversationListSiderRef {
getConversationInfo: () =>
| {
conversationId: string;
sectionId?: string;
}
| undefined;
handleCreateConversation: () => Promise<void>;
handleDeleteConversation: (conversation: Conversation) => Promise<Boolean>;
}
export const ConversationList = forwardRef<
ConversationListSiderRef,
{
onRename: (conversation: Conversation) => void;
onDelete: (conversation: Conversation) => void;
loading: boolean;
groupedConversations: Map<ConversationSort, SortedConversationItem[]>;
conversations: Conversation[];
hasMore: boolean;
loadMore: () => Promise<void>;
}
>(
(
{
onRename,
onDelete,
loading,
groupedConversations,
conversations,
hasMore,
loadMore,
},
ref,
) => {
const {
currentConversationInfo,
updateCurrentConversationInfo,
cozeApi,
updateConversations,
} = useChatAppStore(
useShallow(state => ({
currentConversationInfo: state.currentConversationInfo,
updateCurrentConversationInfo: state.updateCurrentConversationInfo,
cozeApi: state.cozeApi,
updateConversations: state.updateConversations,
})),
);
const conversationRef = useRef<ChatState['currentConversationInfo']>();
const [addLoading, setAddLoading] = useState(false);
const {
layout,
chatConfig: { bot_id: botId, auth: { connectorId } = {} },
} = useChatAppProps();
const isMobile = useMemo(() => layout === Layout.MOBILE, [layout]);
const listContainerRef = useRef<HTMLDivElement>(null);
const loadMoreRef = useRef<HTMLDivElement>(null);
const handleCreateConversation = async () => {
if (!currentConversationInfo) {
return;
}
try {
const res = await cozeApi?.conversations.create({
bot_id: botId,
// @ts-expect-error: 有这个属性,但是 openapi 没有暴露
connector_id: connectorId,
});
if (res?.id) {
conversationRef.current = {
...currentConversationInfo,
id: res.id,
last_section_id: res.last_section_id,
};
updateConversations([res], 'add');
updateCurrentConversationInfo({
...currentConversationInfo,
...res,
name: '',
});
Toast.info({
content: I18n.t('web_sdk_create_conversation', {}, '已创建新会话'),
showClose: false,
});
}
} catch (error) {
console.error(error);
}
};
const handleConversationChange = (conversation: Conversation) => {
if (
!currentConversationInfo ||
conversation.id === currentConversationInfo?.id
) {
return;
}
const c = {
...currentConversationInfo,
...conversation,
id: conversation.id,
sectionId: conversation.last_section_id,
};
conversationRef.current = c;
updateCurrentConversationInfo(c);
};
const handleDeleteConversation = async (conversation: Conversation) => {
try {
const res = (await cozeApi?.delete(
`/v1/conversations/${conversation.id}`,
)) as {
code: number;
};
if (res.code !== 0) {
return false;
}
await updateConversations([conversation], 'remove');
return true;
} catch (error) {
console.error(error);
return false;
}
};
useImperativeHandle(ref, () => ({
getConversationInfo: () => {
if (conversationRef.current) {
return {
conversationId: conversationRef.current.id,
sectionId: conversationRef.current.last_section_id,
};
} else if (conversations.length > 0) {
return {
conversationId: conversations[0].id,
sectionId: conversations[0].last_section_id,
};
}
return undefined;
},
handleCreateConversation,
handleDeleteConversation,
}));
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading) {
loadMore();
}
},
{
root: listContainerRef.current,
rootMargin: '100px',
threshold: 0.01,
},
);
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current);
}
return () => {
if (loadMoreRef.current) {
observer.unobserve(loadMoreRef.current);
}
};
}, [hasMore, loading, loadMore]);
return (
<div className={s.conversations}>
<div className={s['conversations-header']}>
<span className={s['conversations-header-title']}>
{I18n.t('web_sdk_conversation_history', {}, '会话历史')}
</span>
<IconButton
color="secondary"
onClick={() => {
if (currentConversationInfo) {
updateCurrentConversationInfo({
...currentConversationInfo,
conversationListVisible: false,
});
}
}}
icon={<IconCozSideNav width="18px" height="18px" />}
/>
</div>
<Button
size="large"
icon={<IconCozPlus />}
iconPosition="left"
color="highlight"
className={s['conversations-create-button']}
onClick={async () => {
setAddLoading(true);
await handleCreateConversation();
setAddLoading(false);
}}
loading={addLoading}
>
{I18n.t('web_sdk_add_new_conversation', {}, '创建新会话')}
</Button>
<div ref={listContainerRef} className={s['conversations-list']}>
{Array.from(groupedConversations.entries()).map(
([sort, conversationList]) => (
<div key={sort} className={s['conversations-list-group']}>
{conversationList.map((conversation, index) =>
isMobile ? (
<MobileConversationItem
isActive={conversation.id === currentConversationInfo?.id}
key={conversation.id}
item={conversation}
shouldDisplayTime={index === 0}
onConversationChange={handleConversationChange}
onRename={onRename}
onDelete={onDelete}
/>
) : (
<PcConversationItem
isActive={conversation.id === currentConversationInfo?.id}
key={conversation.id}
item={conversation}
shouldDisplayTime={index === 0}
onConversationChange={handleConversationChange}
onRename={onRename}
onDelete={onDelete}
/>
),
)}
</div>
),
)}
<div ref={loadMoreRef} style={{ height: '10px', flexShrink: 0 }} />
{loading ? (
<Spin
style={{
width: '100%',
}}
></Spin>
) : null}
</div>
</div>
);
},
);

View File

@@ -0,0 +1,17 @@
.conversations-container {
display: flex;
height: 100%;
width: 100%;
}
.conversations-side-sheet {
z-index: calc(var(--chat-z-index-input) + 1);
padding: 0;
}
.conversations-list-delete-modal-text {
color: var(--coz-fg-secondary);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}

View File

@@ -0,0 +1,256 @@
/*
* 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 complexity */
import {
Fragment,
type ReactNode,
useState,
forwardRef,
useRef,
useImperativeHandle,
} from 'react';
import { useShallow } from 'zustand/react/shallow';
import { I18n } from '@coze-arch/i18n';
import { IconCozSideNav } from '@coze-arch/coze-design/icons';
import { Input, Modal, SideSheet } from '@coze-arch/coze-design';
import { type Conversation } from '@coze/api';
import { useChatAppStore } from '../studio-open-chat/store';
import {
useConversationList,
useGroupedConversations,
} from '../studio-open-chat/hooks/use-conversation-list';
import {
ConversationList,
type ConversationListSiderRef,
} from './conversation-list';
import s from './index.module.less';
export const ConversationListSider = forwardRef<
Pick<ConversationListSiderRef, 'getConversationInfo'>,
{
children: ReactNode;
}
>(({ children }, ref) => {
const conversationListRef = useRef<ConversationListSiderRef>(null);
const { currentConversationInfo, updateConversations, cozeApi } =
useChatAppStore(
useShallow(state => ({
currentConversationInfo: state.currentConversationInfo,
updateConversations: state.updateConversations,
cozeApi: state.cozeApi,
})),
);
const { loading, conversations, hasMore, loadMore } = useConversationList();
const groupedConversations = useGroupedConversations(conversations);
const [isModalLoading, setIsModalLoading] = useState(false);
const [modalInfo, setModalInfo] = useState<{
visible: boolean;
type: 'rename' | 'delete';
conversation: Conversation;
} | null>(null);
const handleOpenRenameModal = (conversation: Conversation) => {
setModalInfo({
visible: true,
type: 'rename',
conversation,
});
};
const handleOpenDeleteModal = (conversation: Conversation) => {
setModalInfo({
visible: true,
type: 'delete',
conversation,
});
};
const handleUpdateConversationName = async (conversation: Conversation) => {
try {
const res = (await cozeApi?.put(`/v1/conversations/${conversation.id}`, {
name: conversation.name,
})) as {
data: Conversation;
code: number;
};
if (res.code !== 0) {
return;
}
await updateConversations([res.data], 'update');
} catch (error) {
console.error(error);
}
};
useImperativeHandle(ref, () => ({
getConversationInfo: () => {
if (conversationListRef.current) {
return conversationListRef.current.getConversationInfo();
}
return undefined;
},
}));
const handleModalOk = async () => {
if (!modalInfo) {
return;
}
setIsModalLoading(true);
try {
if (modalInfo?.type === 'rename') {
await handleUpdateConversationName(modalInfo.conversation);
} else {
if (modalInfo.conversation.id === currentConversationInfo?.id) {
if (
await conversationListRef.current?.handleDeleteConversation(
modalInfo.conversation,
)
) {
await conversationListRef.current?.handleCreateConversation();
}
} else {
await conversationListRef.current?.handleDeleteConversation(
modalInfo.conversation,
);
}
}
setIsModalLoading(false);
setModalInfo(null);
} catch (error) {
console.error(error);
setIsModalLoading(false);
}
};
return (
<div className={s['conversations-container']}>
{currentConversationInfo?.conversationListVisible &&
currentConversationInfo?.isLargeWidth ? (
<ConversationList
ref={conversationListRef}
onRename={handleOpenRenameModal}
onDelete={handleOpenDeleteModal}
loading={loading}
groupedConversations={groupedConversations}
conversations={conversations}
hasMore={hasMore}
loadMore={loadMore}
/>
) : null}
<Fragment key={currentConversationInfo?.id}>{children}</Fragment>
<SideSheet
visible={
currentConversationInfo?.conversationListVisible &&
!currentConversationInfo?.isLargeWidth
}
closeIcon={<IconCozSideNav />}
closable={false}
closeOnEsc={false}
maskClosable={false}
getPopupContainer={() =>
document.querySelector('.coze-chat-sdk') as HTMLElement
}
placement="left"
width={320}
className={s['conversations-side-sheet']}
headerStyle={{
display: 'none',
}}
bodyStyle={{
padding: 0,
height: '100%',
}}
>
<ConversationList
ref={conversationListRef}
onRename={handleOpenRenameModal}
onDelete={handleOpenDeleteModal}
loading={loading}
groupedConversations={groupedConversations}
conversations={conversations}
hasMore={hasMore}
loadMore={loadMore}
/>
</SideSheet>
<Modal
getPopupContainer={() =>
document.querySelector('.coze-chat-sdk') as HTMLElement
}
okButtonProps={{
loading: isModalLoading,
}}
visible={modalInfo?.visible}
onCancel={() => setModalInfo(null)}
title={
modalInfo?.type === 'delete'
? I18n.t('web_sdk_delete_conversation', {}, '删除会话')
: I18n.t('web_sdk_rename_conversation', {}, '重命名会话')
}
onOk={handleModalOk}
okText={
modalInfo?.type === 'delete'
? I18n.t('web_sdk_delete', {}, '删除')
: I18n.t('web_sdk_confirm', {}, '确定')
}
cancelText={I18n.t('web_sdk_cancel', {}, '取消')}
okButtonColor={modalInfo?.type === 'delete' ? 'red' : 'brand'}
closable={false}
maskClosable={false}
style={{
maxWidth: '80%',
}}
>
{modalInfo?.type === 'rename' ? (
<Input
placeholder={I18n.t(
'web_sdk_conversation_placeholder',
{},
'请输入会话名称',
)}
value={modalInfo.conversation.name}
maxLength={100}
onChange={value =>
setModalInfo({
...modalInfo,
conversation: {
...modalInfo.conversation,
name: value,
},
})
}
/>
) : (
<span className={s['conversations-list-delete-modal-text']}>
{I18n.t(
'web_sdk_conversation_delete_content',
{},
'删除后,会话将无法恢复,确认要删除吗?',
)}
</span>
)}
</Modal>
</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 React, { type FC, type PropsWithChildren } from 'react';
import { ErrorBoundary as FlowErrorBoundary } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { Typography } from '@coze-arch/bot-semi';
import { studioOpenClientReporter } from '@/helper';
const { Title, Text } = Typography;
const FallbackComponent: FC = () => (
<div>
<Title>{I18n.t('404_title')}</Title>
<Text>{I18n.t('404_content')}</Text>
</div>
);
export const ErrorBoundary: FC<PropsWithChildren> = ({ children }) => (
<FlowErrorBoundary
errorBoundaryName="ErrorBoundary"
logger={studioOpenClientReporter.getLogger()}
FallbackComponent={FallbackComponent}
>
{children}
</FlowErrorBoundary>
);

View File

@@ -0,0 +1,32 @@
.wrapper {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
}
.icon {
width: 48px;
height: 48px;
}
.message {
margin-top: 10px;
line-height: 22px;
font-size: 16px;
font-weight: 500;
color: #060709cc;
}
.extra {
margin-top: 4px;
line-height: 16px;
font-size: 12px;
color: #0607094d;
}
.btn {
margin-top: 28px;
}

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 { type FC } from 'react';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import {
getServerError,
type SDKInitError,
ServerErrorCode,
specCodeList,
} from '@/util/error';
import ErrorUnbindPng from '@/assets/error-unbind.png';
import ErrorDefaultPng from '@/assets/error-default.png';
import styles from './index.module.less';
export interface InitErrorFallback {
/**
* null 表示未获取到报错信息
*/
error: SDKInitError | null;
onBeforeRetry?: () => void;
refresh?: () => void;
}
const ErrorFallback: FC<InitErrorFallback> = ({
error,
onBeforeRetry,
refresh,
}) => {
let msg = I18n.t('overview_bi_assistant_system_error');
if (error) {
msg = error.msg;
const wrapError = getServerError(error);
if (wrapError) {
msg = wrapError.msg;
}
}
const defaultError = I18n.t('web_sdk_retry_notification');
const hideExtra = !!error?.code && specCodeList.includes(error.code);
return (
<div className={styles.wrapper}>
<img className={styles.icon} src={getErrorIcon(error)} />
<div className={styles.message}>{msg}</div>
{!hideExtra && (
<div className={styles.extra}>
{I18n.t(
`web_sdk_api_error_${error?.code}` as I18nKeysNoOptionsType,
{},
defaultError,
)}
</div>
)}
<Button
className={styles.btn}
onClick={() => {
onBeforeRetry?.();
if (refresh) {
refresh?.();
} else {
location.reload();
}
}}
>
{I18n.t('retry')}
</Button>
</div>
);
};
export default ErrorFallback;
export const getErrorIcon = (error: SDKInitError | null) => {
switch (error?.code) {
case ServerErrorCode.BotUnbind:
return ErrorUnbindPng;
default:
return ErrorDefaultPng;
}
};

View File

@@ -0,0 +1,34 @@
.footer {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
height: 32px;
font-size: 12px;
font-weight: 400;
background: none;
position: absolute;
width: 100%;
bottom: 2px;
z-index: 50;
> .text {
color: rgba(48, 59, 94, 47%);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
> .link {
color: #4d53e8;
text-decoration: none;
}
}
&.bg-theme {
> .text {
color: rgba(255, 255, 255, 60%);
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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, Fragment } from 'react';
import cls from 'classnames';
import { cozeOfficialHost } from '@coze-studio/open-env-adapter';
import { I18n } from '@coze-arch/i18n';
import { type FooterConfig } from '@/types/client';
import styles from './index.module.less';
const getDefaultText = () =>
I18n.t('web_sdk_official_banner', {
docs_link: (
<a
key="web_sdk_official_banner"
className={styles.link}
href={cozeOfficialHost}
target="_blank"
>
{I18n.t('web_sdk_official_banner_link')}
</a>
),
});
const getTextByExpress = (
expressionText: string,
linkvars?: Record<
string,
{
text: string;
link: string;
}
>,
) => {
const arrLinks: React.ReactNode[] = [];
const splitLinkTag = '{{{link}}}';
const textWithLinkTags = expressionText.replace(
/\{\{\s*(\w+)\s*\}\}/g,
(_, key) => {
const { link, text: linkText } = linkvars?.[key] || {};
if (link && linkText) {
arrLinks.push(
<a className={styles.link} href={link} target="_blank">
{linkText}
</a>,
);
return splitLinkTag;
} else {
arrLinks.push(linkText || '');
}
return splitLinkTag;
},
);
return textWithLinkTags.split(splitLinkTag).map((item, index) => (
<Fragment key={`text_link_${index}`}>
{item}
{arrLinks[index]}
</Fragment>
));
};
const ChatFooter: FC<
FooterConfig & {
footerClassName?: string;
textClassName?: string;
theme?: 'bg-theme' | 'light';
}
> = ({
isShow = true,
expressionText,
linkvars,
footerClassName,
textClassName,
theme,
}) =>
isShow ? (
<footer
className={cls(styles.footer, footerClassName, {
[styles['bg-theme']]: theme === 'bg-theme',
})}
>
<span className={cls(styles.text, textClassName)}>
{expressionText
? getTextByExpress(expressionText, linkvars)
: getDefaultText()}
</span>
</footer>
) : null;
export default ChatFooter;

View File

@@ -0,0 +1,5 @@
.float-open-conversations-btn {
position: absolute;
top: 20px;
left: 0;
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { I18n } from '@coze-arch/i18n';
import { IconCozArrowRightFill } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import { useChatAppStore } from '../studio-open-chat/store';
import { type ChatHeaderProps } from './type';
import ChatHeaderPC from './pc';
import ChatHeaderMobile from './mobile';
import styles from './index.module.less';
const FloatBtn = () => {
const { updateCurrentConversationInfo, currentConversationInfo } =
useChatAppStore(
useShallow(s => ({
updateCurrentConversationInfo: s.updateCurrentConversationInfo,
currentConversationInfo: s.currentConversationInfo,
})),
);
return currentConversationInfo?.conversationListVisible ? null : (
<Tooltip content={I18n.t('web_sdk_open_conversations')}>
<IconButton
className={styles['float-open-conversations-btn']}
size="small"
style={{
height: '32px',
borderTopLeftRadius: 'unset',
borderBottomLeftRadius: 'unset',
}}
icon={<IconCozArrowRightFill />}
onClick={() => {
if (!currentConversationInfo) {
return;
}
updateCurrentConversationInfo({
...currentConversationInfo,
conversationListVisible: true,
});
}}
></IconButton>
</Tooltip>
);
};
export const ChatHeader: FC<ChatHeaderProps & { isMobile?: boolean }> = ({
isMobile,
...props
}) => {
const { isShowConversations, isShowHeader } = props;
if (!isShowHeader) {
return isShowConversations ? <FloatBtn /> : null;
}
return isMobile ? (
<ChatHeaderMobile {...props} />
) : (
<ChatHeaderPC {...props} />
);
};

View File

@@ -0,0 +1,43 @@
/* stylelint-disable declaration-no-important */
.header {
flex: 0 0 auto;
display: flex;
align-items: center;
height: 72px;
padding: 24px 16px 16px;
.conversation-list-btn {
margin-right: 8px;
}
.avatar {
width: 28px;
height: 28px;
margin-right: 15px;
}
.title {
flex: 1 1 auto;
font-size: 20px;
margin-right: 12px;
font-weight: 700;
color: #000000d9;
}
.icon-btn {
width: 32px;
height: 32px;
}
&.bg-theme {
.icon-btn,
.title {
color: #fff;
}
.icon-btn:disabled,
.icon-btn:hover {
color: rgba(255, 255, 255, 80%) !important;
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import cls from 'classnames';
import { IconCozSideNav } from '@coze-arch/coze-design/icons';
import { Button, IconButton } from '@coze-arch/coze-design';
import { Typography } from '@coze-arch/bot-semi';
import { useChatAppStore } from '@/components/studio-open-chat/store';
import {
useChatChatButtonInfo,
useChatOpInfo,
} from '@/components/studio-open-chat/hooks/use-chat-op-info';
import CozeLogoPng from '@/assets/coze-logo.png';
import { type ChatHeaderProps } from '../type';
import styles from './index.module.less';
const ChatHeaderMobile = ({
iconUrl = CozeLogoPng,
title = 'Coze Bot',
extra,
theme,
isShowConversations,
}: ChatHeaderProps) => {
const { headerTopLeftOps } = useChatOpInfo();
const buttonList = useChatChatButtonInfo(headerTopLeftOps);
const { updateCurrentConversationInfo, currentConversationInfo } =
useChatAppStore(
useShallow(s => ({
updateCurrentConversationInfo: s.updateCurrentConversationInfo,
currentConversationInfo: s.currentConversationInfo,
})),
);
return (
<header
className={cls(styles.header, {
[styles['bg-theme']]: theme === 'bg-theme',
})}
>
{currentConversationInfo?.conversationListVisible ||
!isShowConversations ? null : (
<IconButton
color="secondary"
icon={<IconCozSideNav width="18px" height="18px" />}
className={styles['conversation-list-btn']}
onClick={() => {
if (!currentConversationInfo) {
return;
}
updateCurrentConversationInfo({
...currentConversationInfo,
conversationListVisible: true,
});
}}
/>
)}
<img className={styles.avatar} src={iconUrl} alt="avatar" />
<Typography.Text
className={styles.title}
ellipsis={{
rows: 1,
}}
>
{title}
</Typography.Text>
{buttonList?.map(item => (
<Button
color="secondary"
icon={item.icon}
className={styles['icon-btn']}
disabled={item.disabled}
onClick={() => {
item.onClick?.();
}}
/>
))}
{!!extra && extra}
</header>
);
};
export default ChatHeaderMobile;

View File

@@ -0,0 +1,44 @@
/* stylelint-disable declaration-no-important */
.header {
flex: 0 0 auto;
display: flex;
align-items: center;
height: 56px;
padding: 0 16px;
border-bottom: 1px solid #1d1c2314;
.conversation-list-btn {
margin-right: 8px;
}
.avatar {
width: 24px;
height: 24px;
margin-right: 8px;
}
.title {
flex: 1 1 auto;
margin-right: 12px;
font-size: 18px;
font-weight: 600;
color: #1c1d23;
}
.icon-btn {
width: 32px;
height: 32px;
}
&.bg-theme {
.icon-btn,
.title {
color: #fff;
}
.icon-btn:disabled,
.icon-btn:hover {
color: rgba(255, 255, 255, 80%)!important;
}
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import cls from 'classnames';
import { IconCozSideNav } from '@coze-arch/coze-design/icons';
import { Button, IconButton } from '@coze-arch/coze-design';
import { Typography } from '@coze-arch/bot-semi';
import { useChatAppStore } from '@/components/studio-open-chat/store';
import {
useChatChatButtonInfo,
useChatOpInfo,
} from '@/components/studio-open-chat/hooks/use-chat-op-info';
import CozeLogoPng from '@/assets/coze-logo.png';
import { type ChatHeaderProps } from '../type';
import styles from './index.module.less';
const ChatHeader = ({
iconUrl = CozeLogoPng,
title = 'Coze Bot',
extra,
theme,
isShowConversations,
}: ChatHeaderProps) => {
const { headerTopLeftOps } = useChatOpInfo();
const buttonList = useChatChatButtonInfo(headerTopLeftOps);
const { updateCurrentConversationInfo, currentConversationInfo } =
useChatAppStore(
useShallow(s => ({
updateCurrentConversationInfo: s.updateCurrentConversationInfo,
currentConversationInfo: s.currentConversationInfo,
})),
);
// 清空上下文已存在,且需要删除聊天记录按钮
return (
<header
className={cls(styles.header, {
[styles['bg-theme']]: theme === 'bg-theme',
})}
>
{currentConversationInfo?.conversationListVisible ||
!isShowConversations ? null : (
<IconButton
color="secondary"
icon={<IconCozSideNav width="18px" height="18px" />}
className={styles['conversation-list-btn']}
onClick={() => {
if (!currentConversationInfo) {
return;
}
updateCurrentConversationInfo({
...currentConversationInfo,
conversationListVisible: true,
});
}}
/>
)}
<img className={styles.avatar} src={iconUrl} alt="avatar" />
<Typography.Text
className={styles.title}
ellipsis={{
showTooltip: {
opts: { style: { wordBreak: 'break-word' }, position: 'bottom' },
type: 'tooltip',
},
rows: 1,
}}
>
{title}
</Typography.Text>
{buttonList?.map(
item => (
<Button
color="secondary"
icon={item.icon}
className={styles['icon-btn']}
disabled={item.disabled}
onClick={() => {
item.onClick?.();
}}
/>
),
/*
if (item === 'clearMessage') {
return (
<Button
color="secondary"
icon={<IconCozBroom width="18px" height="18px" />}
className={styles['icon-btn']}
onClick={() => {
clearHistory();
}}
/>
);
} else if (item === 'addNewConversation') {
return (
<Button
color="secondary"
icon={<IconAddNewConversation width="18px" height="18px" />}
className={styles['icon-btn']}
onClick={() => {
clearHistory();
}}
/>
);
}
return null;*/
)}
{!!extra && extra}
</header>
);
};
export default ChatHeader;

View File

@@ -16,17 +16,15 @@
import { type ReactNode } from 'react';
export enum Layout {
PC = 'pc',
MOBILE = 'mobile',
}
export interface HeaderConfig {
isShow?: boolean; //Whether to display headers, the default is true
isNeedClose?: boolean; //Whether you need the close button, the default is true.
extra?: ReactNode | false; // For standing, default none
}
export interface DebugProps {
cozeApiRequestHeader?: Record<string, string>;
export interface ChatHeaderProps {
title: string;
iconUrl?: string;
extra?: ReactNode | false;
theme?: 'bg-theme' | 'light';
isNeedTitle?: boolean;
isNeedLogo?: boolean;
isNeedClearMessage?: boolean;
isFixTop?: boolean;
isShowConversations?: boolean;
isShowHeader?: boolean;
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import Icon from '@douyinfe/semi-icons';
export const IconAddNewConversation: FC<{
className?: string;
width?: string;
height?: string;
}> = ({ width, height, className }) => (
<Icon
svg={
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="currentColor"
>
<path
d="M13.4997 16.5H4.49881C2.84338 16.5 1.5 15.1574 1.5 13.5013V4.50088C1.5 2.84333 2.84338 1.5 4.49881 1.5H13.4997C15.158 1.5 16.5 2.84331 16.5 4.50088V13.5013C16.5 15.1574 15.158 16.5 13.4997 16.5ZM15.0013 5.25092C15.0013 4.00792 13.9919 3.00008 12.7496 3.00008H5.24889C4.00657 3.00008 2.99864 4.00792 2.99864 5.25092V12.7498C2.99864 13.9943 4.00657 15.0007 5.24889 15.0007H12.7496C13.9919 15.0007 15.0013 13.9943 15.0013 12.7498V5.25092ZM9.74933 11.9997C9.74933 12.414 9.41351 12.7498 8.99925 12.7498C8.585 12.7498 8.24918 12.414 8.24918 11.9997V9.75115H5.99967C5.58503 9.75115 5.24889 9.41502 5.24889 9.00037C5.24889 8.58572 5.58503 8.24958 5.99967 8.24958H8.24918V6.00099C8.24918 5.58674 8.585 5.25092 8.99925 5.25092C9.41351 5.25092 9.74933 5.58674 9.74933 6.001V8.24958H11.9988C12.4135 8.24958 12.7496 8.58572 12.7496 9.00037C12.7496 9.41502 12.4135 9.75115 11.9988 9.75115H9.74933V11.9997Z"
fill="currentColor"
/>
</svg>
}
className={className}
style={{
width,
height,
}}
/>
);

View File

@@ -0,0 +1,8 @@
.loading {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Spin } from '@coze-arch/coze-design';
import styles from './index.module.less';
export const Loading = () => (
<div className={styles.loading}>
<Spin size="large" />
</div>
);

View File

@@ -0,0 +1,27 @@
.chat-input {
padding: 10px 0;
}
.area {
flex: 1 1 auto;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
* {
box-sizing: border-box;
}
:global(.chat-uikit-on-boarding-pc){
& > div {
padding-right: 14px;
padding-left: 14px;
}
}
.safe-area {
height: 24px;
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC, useEffect, useMemo, useRef } from 'react';
import cs from 'classnames';
import { ChatFlowRender } from '@coze-common/chat-workflow-render';
import {
ChatArea,
useInitStatus,
type ComponentTypesMap,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n/intl';
import { type StudioChatAreaProps } from '@/types/props';
import { Layout } from '@/types/client';
import { useChatAppProps } from '../store';
import { ShortcutBar } from '../components/shortcut-bar';
import { ChatInputLeftSlot } from '../components/chat-input-let-slot';
import styles from './index.module.less';
// eslint-disable-next-line complexity
export const StudioChatArea: FC<StudioChatAreaProps> = ({
coreAreaClassName,
className,
showInputArea = true,
inputPlaceholder,
inputNativeCallbacks,
messageGroupListClassName,
renderChatInputTopSlot,
isShowClearContextDivider,
headerNode,
messageMaxWidth,
isMiniScreen,
enableMultimodalUpload = false,
}) => {
const initStatus = useInitStatus();
const { layout, onInitStateChange, chatConfig } = useChatAppProps();
const refContainer = useRef<HTMLDivElement>(null);
const { readonly } = useChatAppProps();
const chatAreaComponentTypes: Partial<ComponentTypesMap> = useMemo(
() => ({
chatInputIntegration: {
renderChatInputTopSlot: controller => (
<>
{renderChatInputTopSlot?.()}
<ShortcutBar controller={controller} />
</>
),
},
contentBox: ChatFlowRender,
}),
[renderChatInputTopSlot],
);
useEffect(() => {
switch (initStatus) {
case 'initSuccess':
case 'initFail':
onInitStateChange?.(initStatus);
break;
default:
}
}, [initStatus, onInitStateChange]);
if (initStatus !== 'initSuccess') {
return null;
}
const uploadable = chatConfig?.ui?.chatBot?.uploadable ?? true;
const enableLegacyUploadFlag = !enableMultimodalUpload && uploadable;
const enableMultimodalUploadFlag = enableMultimodalUpload && uploadable;
return (
<div
className={cs(styles.area, className, {
[styles.disabled]: readonly,
})}
tabIndex={1000}
ref={refContainer}
>
<ChatArea
classname={coreAreaClassName}
layout={layout === Layout.PC ? undefined : layout}
showInputArea={showInputArea}
newMessageInterruptScenario="never"
messageGroupListClassName={messageGroupListClassName}
showClearContextDivider={
isShowClearContextDivider ||
chatConfig?.ui?.chatBot?.isNeedClearContext
}
messageMaxWidth={messageMaxWidth}
showStopRespond={true}
enableLegacyUpload={enableLegacyUploadFlag}
enableMultimodalUpload={enableMultimodalUploadFlag}
fileLimit={enableMultimodalUploadFlag ? 6 : undefined}
textareaPlaceholder={inputPlaceholder || I18n.t('chatInputPlaceholder')}
enableMessageBoxActionBar={true}
chatInputProps={{
wrapperClassName: styles.chatInput,
inputNativeCallbacks,
safeAreaClassName:
chatConfig?.ui?.footer?.isShow !== false ? styles['safe-area'] : '',
leftActions: <ChatInputLeftSlot />,
}}
componentTypes={chatAreaComponentTypes}
readonly={readonly}
uiKitChatInputButtonConfig={{
isClearContextButtonVisible: false,
isClearHistoryButtonVisible: false,
}}
isMiniScreen={isMiniScreen}
headerNode={headerNode}
/>
</div>
);
};

View File

@@ -0,0 +1,3 @@
.unfocus-text {
color: rgba(48, 68, 112, 38%);
}

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 { I18n } from '@coze-arch/i18n';
import styles from './index.module.less';
export const AudioUnfocusText = () => (
<div className={styles['unfocus-text']}>
{I18n.t('chat_voice_input_need_focus')}
</div>
);

View File

@@ -0,0 +1,37 @@
/* stylelint-disable declaration-no-important */
.container {
padding: 4px;
border-radius: 4px;
@apply coz-bg-max;
.button {
justify-content: flex-start;
width: 100%;
padding-right: 8px;
padding-left: 8px;
font-weight: 400;
text-align: left;
border-radius: 4px;
@apply coz-fg-primary;
&:global(.coz-btn-secondary) {
@apply coz-fg-primary;
}
}
}
.popover {
border-radius: 4px;
}
.chat-input-left-slot {
.disabled {
background-color: rgba(249, 249, 249, 80%)!important;
color: rgba(55, 67 , 106, 38%)!important;
}
}

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 { type FC, useEffect, useState } from 'react';
import cls from 'classnames';
import { OutlinedIconButton, UIKitTooltip } from '@coze-common/chat-uikit';
import { IconCozMore } from '@coze-arch/coze-design/icons';
import { Button, Popover, Space } from '@coze-arch/coze-design';
import { Layout } from '@/types/client';
import { useChatAppProps } from '../../store';
import { useIsShowBackground } from '../../hooks/use-is-show-background';
import {
type ButtonProps,
useChatChatButtonInfo,
useChatOpInfo,
} from '../../hooks/use-chat-op-info';
import styles from './index.module.less';
const MoreBtn: FC<{
buttonList: ButtonProps[];
}> = ({ buttonList }) => {
const showBackground = useIsShowBackground();
const [visible, setVisible] = useState(false);
const { readonly } = useChatAppProps();
useEffect(() => {
document.addEventListener('click', () => {
setVisible(false);
});
return () => {
setVisible(false);
};
}, []);
return (
<Popover
content={
<Space className={styles.container} vertical spacing={0}>
{buttonList?.map((item, index) => (
<Button
color="secondary"
className={styles.button}
icon={item?.icon}
iconPosition="left"
onClick={item?.onClick}
disabled={item?.disabled}
key={index}
>
{item?.text}
</Button>
))}
</Space>
}
trigger="custom"
visible={visible}
position="topLeft"
style={{
borderRadius: '4px',
}}
>
<OutlinedIconButton
data-testid="bot-edit-debug-chat-clear-button"
showBackground={showBackground}
disabled={readonly}
icon={<IconCozMore className="text-18px" />}
size="default"
onClick={e => {
e.stopPropagation();
setVisible(visibleTemp => !visibleTemp);
}}
className={cls('mr-12px', '!rounded-full')}
/>
</Popover>
);
};
export const ChatInputLeftSlot = () => {
const { chatInputLeftOps } = useChatOpInfo();
const showBackground = useIsShowBackground();
const { chatConfig } = useChatAppProps();
const isMobile = chatConfig.ui?.base?.layout === Layout.MOBILE;
const buttonClass = showBackground ? '!coz-fg-images-white' : '';
const buttonList = useChatChatButtonInfo(chatInputLeftOps);
if (chatInputLeftOps.length === 0) {
return null;
}
return (
<div className={styles['chat-input-left-slot']}>
{buttonList.length > 1 ? <MoreBtn buttonList={buttonList} /> : null}
{buttonList.length === 1 ? (
<UIKitTooltip content={buttonList[0].text} hideToolTip={isMobile}>
<OutlinedIconButton
data-testid="bot-edit-debug-chat-clear-button"
showBackground={showBackground}
disabled={buttonList[0].disabled}
icon={buttonList[0].icon}
onClick={e => {
buttonList[0].onClick?.();
}}
className={cls('mr-12px', '!rounded-full', buttonClass, {
[styles.disabled]: buttonList[0].disabled,
})}
/>
</UIKitTooltip>
) : null}
</div>
);
};

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef } from 'react';
import classNames from 'classnames';
import {
ShortcutBar as ChatAreaShortcutBar,
getUIModeByBizScene,
type ShortCutCommand,
} from '@coze-common/chat-area-plugins-chat-shortcuts';
import { type ChatInputIntegrationController } from '@coze-common/chat-area';
import { ToolType } from '@coze-arch/idl/playground_api';
import { useChatAppStore } from '../../store';
import { useIsShowBackground } from '../../hooks/use-is-show-background';
export interface ShortcutBarRenderProps {
controller: ChatInputIntegrationController;
onShortcutActive?: (shortcut: ShortCutCommand | undefined) => void;
}
export const ShortcutBar = ({
controller,
onShortcutActive,
}: ShortcutBarRenderProps) => {
const activeShortcutRef = useRef<ShortCutCommand | undefined>(undefined);
const showBackground = useIsShowBackground();
const shortcuts = useChatAppStore(store => store.shortcuts);
const defaultId = shortcuts.at(0)?.command_id;
if (!shortcuts?.length) {
return null;
}
return (
<ChatAreaShortcutBar
shortcuts={shortcuts}
wrapperClassName={classNames('w-full pl-[68px] pr-[24px] pb-[10px]')}
uiMode={getUIModeByBizScene({
bizScene: 'websdk',
showBackground,
})}
defaultId={defaultId}
onActiveShortcutChange={(shortcutInfo, isTemplateShortcutActive) => {
activeShortcutRef.current = shortcutInfo;
// 开启template快捷指令时隐藏输入框&快捷指令bar
const chatInputSlotVisible = !isTemplateShortcutActive;
controller.setChatInputSlotVisible(chatInputSlotVisible);
onShortcutActive?.(shortcutInfo);
}}
onBeforeSendTemplateShortcut={({ message, options, shortcut }) => {
const parameters = {};
Object.entries(
(
options?.extendFiled?.toolList as Array<{
plugin_id: string;
plugin_api_name: string;
parameters: Record<
string,
{
value: string;
resource_type: 'uri' | '';
}
>;
}>
)?.[0]?.parameters || {},
).map(item => {
const [key, value] = item;
parameters[key] = value.value;
});
const optionsNew = options || {};
if (!optionsNew.extendFiled) {
optionsNew.extendFiled = {};
}
if (
shortcut.tool_type === ToolType.ToolTypePlugin ||
shortcut.tool_type === ToolType.ToolTypeWorkFlow
) {
optionsNew.extendFiled.shortcut_command = {
command_id: shortcut.command_id,
parameters,
};
}
return {
message,
options,
};
}}
/>
);
};

View File

@@ -0,0 +1,61 @@
/*
* 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 { describe, test, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { type StudioChatProviderProps } from '@/types/props';
import { OpenApiSource } from '@/types/open';
import { useUserInfo } from '../use-user-info';
import { ChatPropsProvider } from '../../store/context';
vi.hoisted(() => {
// @ts-expect-error -- 将 IS_OVERSEA 提升到最外层
global.IS_OVERSEA = false;
});
vi.mock('@/components/conversation-list-sider', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
ConversationListSider: () => <div></div>,
}));
describe('user-info', () => {
const testProps: StudioChatProviderProps = {
chatConfig: {
bot_id: 'test',
source: OpenApiSource.WebSdk,
conversation_id: 'test',
},
userInfo: {
id: 'test-id',
nickname: 'test-nickname',
url: 'test-url',
},
};
test('test props first', () => {
const { result: userInfo } = renderHook(useUserInfo, {
wrapper: props => (
<ChatPropsProvider appProps={testProps}>
{props.children}
</ChatPropsProvider>
),
});
expect(userInfo.current?.nickname).toBe('test-nickname');
});
});

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 { useUserInfo } from './use-user-info';
export { useError } from './use-error';

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 { useMemo } from 'react';
import {
useBuiltinButtonStatus,
useChatAreaController,
useClearContext,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import { IconCozBroom, IconCozChatPlus } from '@coze-arch/coze-design/icons';
import { IconAddNewConversation } from '@/components/icon/add-new-conversation';
import { useChatAppProps } from '../store';
export type ChatOp = 'clearContext' | 'clearMessage' | 'addNewConversation';
export interface ButtonProps {
icon?: React.ReactNode;
text?: string | React.ReactNode;
disabled?: boolean;
onClick?: (event?: React.MouseEvent) => void;
}
export const useChatOpInfo = () => {
const { chatConfig } = useChatAppProps();
const chatInputLeftOps: ChatOp[] = [];
const headerTopLeftOps: ChatOp[] = [];
console.log('useChatOpInfo:', chatConfig);
if (chatConfig?.ui?.header?.isShow) {
if (chatConfig?.ui?.chatBot?.isNeedClearContext) {
chatInputLeftOps.push('clearContext');
if (chatConfig?.ui?.chatBot?.isNeedClearMessage) {
headerTopLeftOps.push('clearMessage');
} else if (chatConfig?.ui?.chatBot?.isNeedAddNewConversation) {
headerTopLeftOps.push('addNewConversation');
}
} else {
// 在实际使用中, clearMessage 和 addNewConversation 不会同时出现,两个功能是重复的,只是为了区分按钮的样式
if (chatConfig?.ui?.chatBot?.isNeedClearMessage) {
chatInputLeftOps.push('clearMessage');
} else if (chatConfig?.ui?.chatBot?.isNeedAddNewConversation) {
chatInputLeftOps.push('addNewConversation');
}
}
} else {
if (chatConfig?.ui?.chatBot?.isNeedClearContext) {
chatInputLeftOps.push('clearContext');
}
// 在实际使用中, clearMessage 和 addNewConversation 不会同时出现,两个功能是重复的,只是为了区分按钮的样式
if (chatConfig?.ui?.chatBot?.isNeedClearMessage) {
chatInputLeftOps.push('clearMessage');
} else if (chatConfig?.ui?.chatBot?.isNeedAddNewConversation) {
chatInputLeftOps.push('addNewConversation');
}
}
return {
chatInputLeftOps,
headerTopLeftOps,
};
};
export const useChatChatButtonInfo = (opList: ChatOp[]) => {
const { isClearHistoryButtonDisabled, isClearContextButtonDisabled } =
useBuiltinButtonStatus({});
const { readonly } = useChatAppProps();
const { clearHistory } = useChatAreaController();
const clearContext = useClearContext();
const buttonList = useMemo<ButtonProps[]>(
() =>
opList.map(item => {
if (item === 'addNewConversation') {
return {
icon: <IconAddNewConversation width="18px" height="18px" />,
text: I18n.t('web_sdk_add_new_conversation'),
disabled: isClearHistoryButtonDisabled || readonly,
onClick: () => {
clearHistory?.();
},
};
} else if (item === 'clearContext') {
return {
icon: <IconCozChatPlus width="18px" height="18px" />,
text: I18n.t('store_start_new_chat'),
disabled: isClearContextButtonDisabled || readonly,
onClick: () => {
clearContext();
},
};
} else {
return {
icon: <IconCozBroom width="18px" height="18px" />,
text: I18n.t('coze_home_delete_btn'),
disabled: isClearHistoryButtonDisabled || readonly,
onClick: () => {
clearHistory?.();
},
};
}
}),
[
opList,
isClearContextButtonDisabled,
isClearHistoryButtonDisabled,
readonly,
clearHistory,
clearContext,
],
);
return buttonList;
};

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import dayjs from 'dayjs';
import { type Conversation, type ListConversationReq } from '@coze/api';
import {
ConversationSort,
type SortedConversationItem,
} from '@/types/conversations';
import { type ChatState } from '../store/store';
import { useChatAppProps, useChatAppStore } from '../store';
import { usePaginationRequest } from './use-pagination-request';
// 扩展ListConversationReq类型以满足PaginationParams约束
type ExtendedListConversationReq = ListConversationReq & {
sort_field: 'created_at' | 'updated_at';
[key: string]: unknown;
};
interface UseConversationListParams {
pageSize?: number;
initialPageNum?: number;
order?: ExtendedListConversationReq['sort_field'];
}
interface UseConversationListReturn {
conversations: Conversation[];
loading: boolean;
hasMore: boolean;
loadMore: () => Promise<void>;
}
export const useConversationList = (
conversationListParams?: UseConversationListParams,
): UseConversationListReturn => {
const {
pageSize = 20,
initialPageNum = 1,
order = 'updated_at',
} = conversationListParams ?? {};
const {
chatConfig: { bot_id: botId, auth: { connectorId } = {} },
} = useChatAppProps();
const {
cozeApiSdk,
currentConversationInfo,
updateCurrentConversationInfo,
conversations,
updateConversations,
} = useChatAppStore(
useShallow(state => ({
cozeApiSdk: state.cozeApi,
conversations: state.conversations,
updateCurrentConversationInfo: state.updateCurrentConversationInfo,
currentConversationInfo: state.currentConversationInfo,
updateConversations: state.updateConversations,
})),
);
const { data, hasMore, loadMore, loading } = usePaginationRequest<
Conversation,
ExtendedListConversationReq
>({
requestFn: async params => {
if (!cozeApiSdk || !botId) {
return { data: [], has_more: false };
}
try {
const result = await cozeApiSdk.conversations.list(params);
return {
data: result.conversations,
has_more: result.has_more,
};
} catch (e) {
console.error(e);
return { data: [], has_more: false };
}
},
requestParams: {
bot_id: botId,
connector_id: connectorId,
sort_field: order,
},
pageSize,
initialPageNum,
autoLoad: !!cozeApiSdk && !!botId,
});
useEffect(() => {
if (data) {
updateConversations(data, 'replace');
}
}, [data]);
useEffect(() => {
if (!currentConversationInfo && data.length > 0) {
const chatContainer = document.querySelector('.coze-chat-sdk');
let info: ChatState['currentConversationInfo'] = {
...data[0],
conversationListVisible: false,
isLargeWidth: false,
};
if (chatContainer && (chatContainer as HTMLElement).offsetWidth >= 780) {
info = {
...info,
conversationListVisible: true,
isLargeWidth: true,
};
}
updateCurrentConversationInfo(info);
}
}, [currentConversationInfo, data]);
return {
conversations,
loading,
hasMore,
loadMore,
};
};
export const useGroupedConversations = (conversations: Conversation[]) => {
const sortedConversations: SortedConversationItem[] = useMemo(() => {
const today = new Date();
const oneDay = 24 * 60 * 60 * 1000;
const thirtyDays = 30 * oneDay;
const newConversationList = conversations
.map(item => {
const dateString = item.updated_at || item.created_at || 0;
const date = dayjs.unix(Number(dateString)).toDate();
const diff = today.getTime() - date.getTime();
if (today.toLocaleDateString() === date.toLocaleDateString()) {
return {
...item,
sort: ConversationSort.Today,
};
} else if (diff < thirtyDays) {
return {
...item,
sort: ConversationSort.In30days,
};
} else {
return {
...item,
sort: ConversationSort.Others,
};
}
})
.sort((a, b) => {
if (a.sort !== b.sort) {
return a.sort - b.sort;
} else {
return (
dayjs.unix(Number(b.updated_at || b.created_at || 0)).valueOf() -
dayjs.unix(Number(a.updated_at || a.created_at || 0)).valueOf()
);
}
});
return newConversationList;
}, [conversations]);
const groupedConversations = useMemo(() => {
const groups = new Map<ConversationSort, SortedConversationItem[]>();
sortedConversations.forEach(conversation => {
if (!groups.has(conversation.sort)) {
groups.set(conversation.sort, []);
}
groups.get(conversation.sort)?.push(conversation);
});
return groups;
}, [sortedConversations]);
return groupedConversations;
};

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 { useState } from 'react';
import { Toast } from '@coze-arch/bot-semi';
import { type SDKInitError } from '@/util/error';
import { catchParse } from '@/util';
type ErrorState = boolean | SDKInitError;
export type SetInitError = (error: ErrorState) => void;
export const useError = () => {
const [initError, setError] = useState<ErrorState>(false);
return {
initError,
setInitError: (error: ErrorState) => {
if (!error) {
setError(error);
} else {
if (initError && typeof initError !== 'boolean') {
return;
}
setError(error);
}
},
onMessageSendFail: (_params, _from, error) => {
if (error instanceof Error) {
const res = catchParse<{ code?: number; msg?: string }>(
error.message,
{},
);
if (res?.code && res?.msg) {
Toast.error(res.msg);
}
}
},
};
};

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.
*/
import { useIsShowBackground } from './use-is-show-background';
export const useGetTheme = () => {
const isShowBackground = useIsShowBackground();
return isShowBackground ? 'bg-theme' : 'light';
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useChatAppProps, useChatAppStore } from '../store';
export const useIsShowBackground = () => {
const backgroundInfo = useChatAppStore(s => s.backgroundInfo);
const { isCustomBackground } = useChatAppProps();
// 自定义背景图,或者背景图有数据,则有背景状态
return (
isCustomBackground ||
!!backgroundInfo?.mobile_background_image?.origin_image_url ||
false
);
};

View File

@@ -0,0 +1,148 @@
/*
* 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, useCallback, useEffect } from 'react';
import { useRequest } from 'ahooks';
interface PaginationParams {
page_num?: number;
page_size?: number;
[key: string]: unknown;
}
interface PaginationResponse<T> {
data: T[];
has_more?: boolean;
total?: number;
[key: string]: unknown;
}
interface UsePaginationRequestParams<T, P extends PaginationParams> {
requestFn: (params: P) => Promise<PaginationResponse<T>>;
requestParams: Omit<P, 'page_num' | 'page_size'>;
pageSize?: number;
initialPageNum?: number;
autoLoad?: boolean;
dataKey?: string;
hasMoreKey?: string;
}
interface UsePaginationRequestReturn<T> {
data: T[];
loading: boolean;
error: Error | undefined;
hasMore: boolean;
currentPage: number;
loadMore: () => Promise<void>;
refresh: () => Promise<void>;
setPageNum: (pageNum: number) => void;
reset: () => Promise<void>;
}
export const usePaginationRequest = <T, P extends PaginationParams>({
requestFn,
requestParams,
pageSize = 20,
initialPageNum = 1,
autoLoad = true,
}: UsePaginationRequestParams<T, P>): UsePaginationRequestReturn<T> => {
const [currentPage, setCurrentPage] = useState(initialPageNum);
const [allData, setAllData] = useState<T[]>([]);
const [hasMore, setHasMore] = useState(true);
const { loading, error, run } = useRequest(
async (pageNum?: number) => {
const targetPage = pageNum ?? currentPage;
const params = {
...requestParams,
page_size: pageSize,
page_num: targetPage,
};
const res = await requestFn(params as P);
return res;
},
{
manual: true,
onSuccess: (res, [pageNum]) => {
const targetPage = pageNum ?? currentPage;
const responseData = res.data;
const responseHasMore = !!res.has_more;
if (targetPage === 1) {
// 如果是第一页,直接替换数据
setAllData(responseData);
} else {
// 如果是加载更多,追加数据
setAllData(prev => [...prev, ...responseData]);
}
setHasMore(responseHasMore);
setCurrentPage(targetPage);
},
onError: err => {
console.error('分页请求失败:', err);
},
},
);
const loadMore = useCallback(async () => {
if (!loading && hasMore) {
await run(currentPage + 1);
}
}, [loading, hasMore, currentPage, run]);
const refresh = useCallback(async () => {
setAllData([]);
setCurrentPage(1);
await run(1);
}, [run]);
const setPageNum = useCallback(
async (pageNum: number) => {
if (pageNum >= 1) {
await run(pageNum);
}
},
[run],
);
const reset = useCallback(async () => {
setAllData([]);
setCurrentPage(initialPageNum);
await setHasMore(false);
}, [initialPageNum]);
// 组件挂载时自动加载第一页
useEffect(() => {
if (autoLoad) {
run(initialPageNum);
}
}, [autoLoad, initialPageNum, run]);
return {
data: allData,
loading,
error,
hasMore,
currentPage,
loadMore,
refresh,
setPageNum,
reset,
};
};

View File

@@ -0,0 +1,52 @@
/*
* 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, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { isEqual } from 'lodash-es';
import { useChatAreaStoreSet } from '@coze-common/chat-area';
import { useChatAppStore } from '../store';
export const useUpdateConversationNameByMessage = () => {
const currentConversationNameRef = useRef<string>();
const { updateCurrentConversationNameByMessage, currentConversationInfo } =
useChatAppStore(
useShallow(s => ({
updateCurrentConversationNameByMessage:
s.updateCurrentConversationNameByMessage,
currentConversationInfo: s.currentConversationInfo,
})),
);
const { useMessagesStore } = useChatAreaStoreSet();
const messages = useMessagesStore(s => s.messages, isEqual);
useEffect(() => {
currentConversationNameRef.current = currentConversationInfo?.name;
}, [currentConversationInfo]);
useEffect(() => {
const message = messages[messages.length - 1];
const name = message?.content.slice(0, 100);
if (message && !currentConversationNameRef.current) {
updateCurrentConversationNameByMessage(name);
currentConversationNameRef.current = name;
}
}, [messages]);
};

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import { nanoid } from 'nanoid';
import { type UserSenderInfo } from '@coze-common/chat-area';
import { useChatAppStore } from '../store';
export const useUserInfo = () => {
const userInfo = useChatAppStore(s => s.userInfo);
return useMemo<UserSenderInfo | null>(() => {
const openUserInfo = userInfo;
if (!openUserInfo) {
return {
id: nanoid(),
nickname: '',
url: '',
userUniqueName: '',
userLabel: null,
};
}
const areaUserInfo: UserSenderInfo = {
...openUserInfo,
userUniqueName: '',
userLabel: null,
};
return areaUserInfo;
}, [userInfo]);
};

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 { OpenChatProvider } from './provider';
export { StudioChatArea } from './area';

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PluginRegistryEntry } from '@coze-common/chat-area';
import { type PluginBizContext } from './types/biz-context';
import { BizPlugin } from './plugin';
export type ChatCommonPlugin = PluginRegistryEntry<PluginBizContext>;
export const getChatCommonPlugin = (props: PluginBizContext) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const WebsdkChatCommonPlugin: ChatCommonPlugin = {
/**
* 贯穿插件生命周期、组件的上下文
*/
createPluginBizContext() {
return { ...props };
},
/**
* 插件本体
*/
Plugin: BizPlugin,
};
return WebsdkChatCommonPlugin;
};

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 {
PluginMode,
PluginName,
WriteableChatAreaPlugin,
createWriteableLifeCycleServices,
} from '@coze-common/chat-area';
import { type PluginBizContext } from './types/biz-context';
import { bizLifeCycleServiceGenerator } from './services/life-cycle';
export class BizPlugin extends WriteableChatAreaPlugin<PluginBizContext> {
/**
* 插件类型
* PluginMode.Readonly = 只读模式
* PluginMode.Writeable = 可写模式
*/
public pluginMode = PluginMode.Writeable;
/**
* 插件名称
* 请点 PluginName 里面去定义
*/
public pluginName = PluginName.WebsdkChatCommonPlugin;
/**
* 生命周期服务
*/
public lifeCycleServices = createWriteableLifeCycleServices(
this,
bizLifeCycleServiceGenerator,
);
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type WriteableAppLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
export const appLifeCycleServiceGenerator: WriteableAppLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({
onInitialError: () => {
plugin.pluginBizContext.onInitialError();
},
});

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type WriteableCommandLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
export const commandLifeCycleServiceGenerator: WriteableCommandLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({
/*onImageClick: async ctx => {
const url = ctx.url;
plugin.pluginBizContext.onImageClick?.({
url
});
}*/
});

View File

@@ -0,0 +1,32 @@
/*
* 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 WriteableLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
import { renderLifeCycleServiceGenerator } from './render';
import { messageLifeCycleServiceGenerator } from './message';
import { commandLifeCycleServiceGenerator } from './command';
import { appLifeCycleServiceGenerator } from './app';
export const bizLifeCycleServiceGenerator: WriteableLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({
appLifeCycleService: appLifeCycleServiceGenerator(plugin),
messageLifeCycleService: messageLifeCycleServiceGenerator(plugin),
commandLifeCycleService: commandLifeCycleServiceGenerator(plugin),
renderLifeCycleService: renderLifeCycleServiceGenerator(plugin),
});

View File

@@ -0,0 +1,52 @@
/*
* 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 WriteableMessageLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { Toast } from '@coze-arch/bot-semi';
import { catchParse } from '@/util';
import { type PluginBizContext } from '../../types/biz-context';
export const messageLifeCycleServiceGenerator: WriteableMessageLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({
onSendMessageError: ctx => {
const { error } = ctx;
if (error instanceof Error) {
const res = catchParse<{ code?: number; msg?: string }>(
error.message,
{},
);
if (res?.code && res?.msg) {
Toast.error(res.msg);
}
}
},
onBeforeSendMessage: ctx => {
const { options = {} } = ctx;
const optionNew = Object.assign({}, options, {
extendFiled: {
...options?.extendFiled,
extra: {
...(options?.extendFiled?.extra || {}),
...(plugin.pluginBizContext.extraBody || {}),
},
},
});
return { ...ctx, options: optionNew };
},
});

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 WriteableRenderLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
export const renderLifeCycleServiceGenerator: WriteableRenderLifeCycleServiceGenerator<
PluginBizContext
> = plugin => ({});

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.
*/
import { type ChatAreaEventCallback } from '@coze-common/chat-area';
export interface PluginBizContext {
onInitialError: () => void;
onImageClick?: ChatAreaEventCallback['onImageClick'];
extraBody?: Record<string, string>;
}

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' />

View File

@@ -0,0 +1,32 @@
/*
* 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 { useSendMessageAdapter } from './use-send-message';
export { useClearMessageContextAdapter } from './use-clear-message-context';
export { useClearHistoryAdapter } from './use-clear-history';
export { useMessageList, useGetMessageListByPairs } from './use-message-list';
export {
useCommonOnAfterResponseHooks,
useCommonOnBeforeRequestHooks,
useCommonErrorResponseHooks,
} from './use-common-hooks';
export {
messageConverterToCoze,
MessageParser,
messageConverterToSdk,
} from './message';
export { useBreakMessage } from './use-break-message';

View File

@@ -0,0 +1,19 @@
/*
* 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 { messageConverterToCoze } from './message-convert-to-coze';
export { messageConverterToSdk } from './message-convert-to-sdk';
export { MessageParser } from './message-parser';

View File

@@ -0,0 +1,197 @@
/*
* 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 { nanoid } from 'nanoid';
import { getFileInfo, FileTypeEnum } from '@coze-studio/file-kit/logic';
import { ContentType } from '@coze-common/chat-core';
import {
type ChatV3Message,
type ContentType as SdkContentType,
type ListMessageData,
} from '@coze/api';
import { catchParse } from '@/util';
interface ObjectStringItem {
type: 'text' | 'image' | 'file';
text?: string;
file_id?: string;
file_url?: string;
}
const microSeconds = 1000;
// 消息转换成 Coze的消息主要用于消息接收后在页面显示。
class MessageConverseToCoze {
public convertMessageListResponse(res: ListMessageData, botId = '') {
const {
data: messageList = [],
has_more: hasMore,
first_id: firstId,
last_id: lastId,
} = res;
const messageListForCoze =
messageList
.map(item => this.convertMessage(item, botId))
.filter(item => !!item.message_id) || [];
console.log('messageListForCoze', messageListForCoze);
return {
code: 0,
message_list: messageListForCoze,
hasmore: hasMore,
cursor: lastId,
next_cursor: firstId,
};
}
public convertMessage(message: ChatV3Message, botId = '') {
const { content_type, content } =
this.convertContent(message.content_type, message.content as string) ||
{};
const isQuestion = message.type === ('question' as ChatV3Message['type']);
const replyId = message.chat_id || `--custom-replyId--${nanoid()}`;
const messageId = isQuestion
? replyId
: message.id || `--custom-messageId-${nanoid()}`; // 无messageId输出一个默认的
const senderId = isQuestion ? '' : message.bot_id || botId;
if (!content_type || !messageId || !replyId) {
return {};
}
let pluginName = '';
if (message.type === 'function_call') {
const contentObj = catchParse<{ plugin: string }>(
message.content as string,
);
pluginName = contentObj?.plugin || '';
}
return {
// @ts-expect-error -- linter-disable-autofix, 新添加参数sdk中还未支持到
reasoning_content: message.reasoning_content,
content,
content_time: (message.created_at || 0) * microSeconds,
content_type,
message_id: messageId,
reply_id: replyId,
role: message.role,
// @ts-expect-error -- linter-disable-autofix, 新添加参数sdk中还未支持到
section_id: message.section_id,
sender_id: senderId, // todo 用户id添加
source: 0, //...
status: '',
extra_info: {
local_message_id: '',
plugin: pluginName,
coze_api_message_id: message.id,
coze_api_chat_id: message.chat_id,
},
type: message.type,
};
}
public convertContent(contentType: SdkContentType, content: string) {
switch (contentType) {
case 'object_string': {
return {
content_type: ContentType.Mix,
content: this.convertMixContent(content),
};
}
case 'card': {
return {
content_type: ContentType.Card,
content,
};
}
case 'text': {
return {
content_type: ContentType.Text,
content,
};
}
default: {
return;
}
}
}
private convertMixContent(content: string) {
const contentObj = catchParse<ObjectStringItem[]>(content);
if (!contentObj) {
return;
}
const itemList = contentObj
?.map(item => {
switch (item.type) {
case 'text': {
return {
type: ContentType.Text,
text: item.text || '',
// @ts-expect-error -- linter-disable-autofix
is_refer: item.is_refer || undefined,
};
}
case 'image': {
return {
type: ContentType.Image,
image: {
key: item?.file_id || '',
image_ori: {
height: undefined,
width: undefined,
url: item?.file_url,
},
image_thumb: {
height: undefined,
width: undefined,
url: item?.file_url,
},
},
// @ts-expect-error -- linter-disable-autofix
is_refer: item.is_refer || undefined,
};
}
case 'file': {
const { fileType = FileTypeEnum.DEFAULT_UNKNOWN } =
// @ts-expect-error -- linter-disable-autofix, 新添加参数sdk中还未支持到
getFileInfo(new File([], item?.name)) || {};
return {
type: ContentType.File,
file: {
file_key: item.file_id || '',
// @ts-expect-error -- linter-disable-autofix, 新添加参数sdk中还未支持到
file_name: item?.name,
// @ts-expect-error -- linter-disable-autofix, 新添加参数sdk中还未支持到
file_size: item?.size,
file_type: fileType,
file_url: item?.file_url,
},
// @ts-expect-error -- linter-disable-autofix
is_refer: item.is_refer || undefined,
};
}
default: {
return;
}
}
})
.filter(item => !!item);
const contentResult = {
item_list: itemList.filter(item => !item.is_refer),
refer_items: itemList.filter(item => item.is_refer),
};
return JSON.stringify(contentResult);
}
}
export const messageConverterToCoze = new MessageConverseToCoze();

View File

@@ -0,0 +1,269 @@
/*
* 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 { ContentType, type MessageContent } from '@coze-common/chat-core';
import { type ShortCutCommand } from '@coze-common/chat-area-plugins-chat-shortcuts';
import { type MixInitResponse, type ChatMessage } from '@coze-common/chat-area';
import {
type EnterMessage,
type RoleType,
type ContentType as CozeApiContentType,
type ObjectStringItem,
} from '@coze/api';
import { catchParse } from '@/util';
import { type OpenUserInfo } from '@/types/user';
class MessageConverterToSdk {
public convertRequestBody({
body,
userInfo,
connectorId,
parameters,
shortcuts,
}: {
body: string;
userInfo?: OpenUserInfo;
connectorId?: string;
parameters?: Record<string, unknown>;
shortcuts?: ShortCutCommand[];
}): string {
const messageBody: Record<string, string> = catchParse(body) || {};
const contentType = messageBody.content_type as ContentType;
const content = messageBody.query as string;
const shortcutCommand = messageBody.shortcut_command as string;
return JSON.stringify({
bot_id: messageBody.bot_id,
user_id: userInfo?.id,
stream: true,
connector_id: connectorId,
additional_messages: [this.convertRequestMessage(contentType, content)],
parameters,
shortcut_command: this.convertShortcuts(shortcuts || [], shortcutCommand),
enable_card: true,
});
}
// 替换 chat请求中的 message部分
private convertRequestMessage(contentType: ContentType, content: string) {
return {
role: 'user',
...this.convertContent(contentType, content),
};
}
private convertContent(
contentType: ContentType,
content: string,
isNeedFileUrl = false,
) {
switch (contentType) {
case ContentType.Text:
return {
content_type: 'text',
content,
};
case ContentType.Card:
return {
content_type: 'card',
content,
};
case ContentType.Image:
case ContentType.File:
case ContentType.Mix: {
return this.convertMixContent(content, isNeedFileUrl);
}
default: {
throw new Error('Error: unknown content Type');
}
}
}
private convertMixContent(content: string, isNeedFileUrl = false) {
const contentObj = catchParse(content) as MessageContent<ContentType.Mix> &
MessageContent<ContentType.File> &
MessageContent<ContentType.Image>;
if (!contentObj) {
return;
}
let mixObjectList: MessageContent<ContentType.Mix>['item_list'] = [
...(contentObj?.item_list || []),
];
const mixReferObjectList: MessageContent<ContentType.Mix>['item_list'] = [
// @ts-expect-error -- linter-disable-autofix
...(contentObj?.refer_items || []),
];
mixObjectList = mixObjectList.concat(
(contentObj?.image_list || []).map(item => ({
type: ContentType.Image,
image: item,
})),
);
mixObjectList = mixObjectList.concat(
(contentObj?.file_list || []).map(item => ({
type: ContentType.File,
file: item,
})),
);
mixObjectList = mixObjectList.concat(
(mixReferObjectList || []).map(item => ({
...item,
is_refer: true,
})),
);
return {
content_type: 'object_string',
content: JSON.stringify(
mixObjectList
.map(item => {
switch (item.type) {
case ContentType.Text:
return {
type: 'text',
text: item.text,
// @ts-expect-error -- linter-disable-autofix
is_refer: item.is_refer || undefined,
};
case ContentType.Image: {
return {
type: 'image',
file_id: item.image.key,
file_url:
isNeedFileUrl || !item.image.key
? item.image.image_ori?.url
: undefined,
// @ts-expect-error -- linter-disable-autofix
is_refer: item.is_refer || undefined,
};
}
case ContentType.File: {
return {
type: 'file',
file_id: item.file.file_key || !item.file.file_key,
file_url: isNeedFileUrl ? item.file?.file_url : undefined,
// @ts-expect-error -- linter-disable-autofix
is_refer: item.is_refer || undefined,
};
}
default: {
return null;
}
}
})
.filter(item => !!item),
),
};
}
public convertMessageListResponse(
messageList: MixInitResponse['messageList'],
) {
return messageList
?.reverse()
.map(item => {
const cozeMessage = this.convertMessage(item);
//(alias) type CozeApiContentType = "text" | "card" | "object_string"
if (cozeMessage?.content_type === 'object_string') {
const contentObj = catchParse(
cozeMessage.content as unknown as string,
) as ObjectStringItem[];
const contentTemp = contentObj?.map(item2 => {
if (item2.type === 'image' || item2.type === 'file') {
return {
type: 'text',
text: item2.file_url,
};
}
return item2;
});
if (contentTemp?.length === 1 && contentTemp[0]?.type === 'text') {
cozeMessage.content_type = 'text' as CozeApiContentType;
cozeMessage.content = contentTemp[0].text;
} else if (contentTemp?.length > 0) {
cozeMessage.content = JSON.stringify(contentTemp);
} else {
return null;
}
}
return cozeMessage;
})
.filter(item => !!item);
}
private convertMessage(message: ChatMessage): EnterMessage | null {
if (
message.type &&
['ack', 'answer', 'question'].includes(message.type) &&
message.role &&
['user', 'assistant'].includes(message.role) &&
message.content_type &&
['card', 'image', 'text', 'object_string', 'file'].includes(
message.content_type,
) &&
message.content
) {
// @ts-expect-error -- linter-disable-autofix
const sdkMessage: EnterMessage = {
role: message.role as RoleType,
...this.convertContent(
message.content_type as ContentType,
message.content || '',
true,
),
};
return sdkMessage;
}
return null;
}
private convertShortcuts(
shortcuts: ShortCutCommand[],
commandStr:
| string
| {
command_id: string;
parameters: Record<string, unknown>;
},
) {
let command;
if (typeof commandStr === 'string') {
command = catchParse(commandStr);
} else if (typeof commandStr === 'object') {
command = commandStr;
} else {
return commandStr;
}
const currentShortcut = shortcuts.find(
item => item.command_id === command.command_id,
);
if (currentShortcut?.components_list) {
const toolParameterMap = new Map(
currentShortcut.components_list
.filter(c => c.parameter && c.name)
.map(c => [c.parameter, c.name]),
);
Object.keys(command.parameters).forEach(key => {
const compName = toolParameterMap.get(key);
const val = command.parameters[key];
if (compName) {
delete command.parameters[key];
command.parameters[compName] = val;
}
});
}
return command;
}
}
export const messageConverterToSdk = new MessageConverterToSdk();

View File

@@ -0,0 +1,251 @@
/*
* 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 { nanoid } from 'nanoid';
import {
ContentType,
type RequestManagerOptions,
type ParsedEvent,
} from '@coze-common/chat-core';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { safeJSONParse } from '@coze-arch/bot-utils';
import { type CreateChatData, type ChatV3Message } from '@coze/api';
import { catchParse } from '@/util';
import { type OpenUserInfo } from '@/types/user';
import { messageConverterToCoze } from './message-convert-to-coze';
type MessageParserFunc = ReturnType<
Required<Required<RequestManagerOptions>['hooks']>['onGetMessageStreamParser']
>;
// 消息解析主要用于从服务端获取到消息后解析成coze能适配的数据结构
enum ChunkEvent {
ERROR = 'error',
DONE = 'done',
MESSAGE_DELTA = 'conversation.message.delta',
MESSAGE_COMPLETED = 'conversation.message.completed',
// 其他消息暂时不处理,中间过程消息。
CHAT_COMPLETED = 'conversation.chat.completed',
CHAT_CREATED = 'conversation.chat.created',
CHAT_FAILED = 'conversation.chat.failed',
}
export class MessageParser {
private seqNo = 0; //标识消息的序号
private indexNo = 0; //标识类型的序号
private indexNoMap: Record<string, number> = {};
private conversationId = '';
private localMessageId = '';
private sendMessageContent = '';
private sendMessageContentType = '';
private botId = '';
private sectionId = '';
private botVersion = '';
private userInfo?: OpenUserInfo;
constructor({
requestMessageRawBody,
userInfo,
sectionId,
}: {
requestMessageRawBody: Record<string, unknown>;
userInfo?: OpenUserInfo;
sectionId?: string;
}) {
this.conversationId = requestMessageRawBody.conversation_id as string;
this.localMessageId = requestMessageRawBody.local_message_id as string;
this.sendMessageContent = requestMessageRawBody.query as string;
this.sendMessageContentType = requestMessageRawBody.content_type as string;
this.botId = requestMessageRawBody.bot_id as string;
this.botVersion = requestMessageRawBody.bot_version as string;
this.userInfo = userInfo;
this.sectionId = sectionId || '';
}
public parse(
parseEvent: Partial<ParsedEvent>,
{ terminate }: { terminate: () => void },
): ParsedEvent | undefined {
const { data, event } = parseEvent;
switch (event) {
case ChunkEvent.CHAT_CREATED: {
return this.createAckMessage(data as string) as unknown as ParsedEvent;
}
case ChunkEvent.MESSAGE_DELTA: {
const message = this.createMessage(data as string);
if (!message) {
return;
}
return message as unknown as ParsedEvent;
}
case ChunkEvent.MESSAGE_COMPLETED: {
return this.createMessage(
data as string,
true,
) as unknown as ParsedEvent;
}
case ChunkEvent.CHAT_COMPLETED:
case ChunkEvent.DONE: {
terminate();
return;
}
// 对话过程中出现异常例如token 消耗完了
case ChunkEvent.CHAT_FAILED: {
const messageError = safeJSONParse(data) as CreateChatData;
const errorMsg = messageError.last_error?.msg || I18n.t('sendFailed');
Toast.error(errorMsg);
throw new Error('Chat stream error');
}
case ChunkEvent.ERROR: {
const messageError = safeJSONParse(data) as {
code: number;
msg: string;
};
const errorMsg = messageError?.msg || I18n.t('sendFailed');
Toast.error(errorMsg);
throw new Error('Chat stream error');
}
default:
return;
}
}
private createMessage(data: string, isComplete = false) {
const dataValue = catchParse<ChatV3Message>(data);
if (!dataValue) {
return;
}
const messageType = dataValue?.type || '';
dataValue.chat_id =
!dataValue.chat_id || dataValue.chat_id === '0' ? '' : dataValue.chat_id;
dataValue.id = !dataValue.id || dataValue.id === '0' ? '' : dataValue.id;
const message = messageConverterToCoze.convertMessage(
dataValue,
this.botId,
);
if (!message) {
return;
}
if (
isComplete &&
message.content_type === ContentType.Text &&
message.type === 'answer'
) {
message.content = '';
}
message.section_id = message.section_id || this.sectionId;
return {
event: 'message',
data: {
conversation_id: this.conversationId,
index: this.getIndexNo(messageType),
is_finish: isComplete,
seq_id: this.getSeqNo(),
message: { ...message, sender: this.botId },
},
};
}
private createAckMessage(data: string) {
const messageType = 'ack';
const dataValue = catchParse<CreateChatData & { execute_id?: string }>(
data,
);
if (!dataValue) {
return;
}
const chatId = dataValue?.id === '0' || !dataValue?.id ? '' : dataValue?.id;
const replyId = chatId || `--custom-replyId--${nanoid()}`;
// @ts-expect-error -- linter-disable-autofix, 新添加参数sdk中还未支持到
const messageId = dataValue.inserted_additional_messages?.lastItem?.id;
return {
event: 'message',
data: {
conversation_id: this.conversationId,
index: this.getIndexNo(messageType),
is_finish: true,
message: {
content: this.sendMessageContent,
content_time: (dataValue?.created_at || 0) * 1000,
content_type: this.sendMessageContentType,
extra_info: {
local_message_id: this.localMessageId,
chatflow_execute_id: dataValue?.execute_id,
coze_api_message_id: messageId,
coze_api_chat_id: chatId,
},
message_id: replyId,
reply_id: replyId,
role: 'user',
// @ts-expect-error -- linter-disable-autofix, 新添加参数sdk中还未支持到
section_id: dataValue?.section_id || this.sectionId, //todo 添加代码
sender_id: this.userInfo?.id,
source: 0, //...
status: '',
type: messageType,
},
seq_id: this.getSeqNo(),
},
};
}
private getSeqNo() {
return this.seqNo++;
}
private getIndexNo(messageType: string) {
if (!this.indexNoMap[messageType]) {
this.indexNoMap[messageType] = this.indexNo++;
}
return this.indexNoMap[messageType];
}
static getMessageParser({
requestMessageRawBody,
userInfo,
sectionId,
}: {
requestMessageRawBody: Record<string, unknown>;
userInfo?: OpenUserInfo;
sectionId?: string;
}): MessageParserFunc {
let parser: MessageParser | undefined = new MessageParser({
requestMessageRawBody,
userInfo,
sectionId,
});
const destroy = () => {
parser = undefined;
};
return (parseEvent, method) => {
const { terminate } = method;
const { type, event } = parseEvent as ParsedEvent & { type: string };
if (type === 'event') {
//
const result = parser?.parse(parseEvent as ParsedEvent, { terminate });
if (
[ChunkEvent.DONE, ChunkEvent.ERROR, ChunkEvent.CHAT_FAILED].includes(
event as ChunkEvent,
)
) {
destroy();
}
return result;
}
};
}
}

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 { useMemo } from 'react';
import { type SceneConfig } from '@coze-common/chat-core';
export const useBreakMessage = (): SceneConfig =>
useMemo(() => {
const config = {
url: '/v3/chat/cancel',
method: 'POST',
hooks: {
onBeforeRequest: [
requestConfig => {
const conversationId = requestConfig.data.conversation_id;
const chatId = requestConfig.data.query_message_id;
return {
...requestConfig,
data: { conversation_id: conversationId, chat_id: chatId },
};
},
],
},
};
return config;
}, []);

View File

@@ -0,0 +1,73 @@
/*
* 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 { useMemo, useRef } from 'react';
import { type SceneConfig } from '@coze-common/chat-core';
import { OpenApiSource } from '@/types/open';
import { useChatAppProps } from '@/components/studio-open-chat/store';
import { type ChatProviderFunc } from '../type';
export const useClearHistoryAdapter = ({
refChatFunc,
}: {
refChatFunc?: React.MutableRefObject<ChatProviderFunc | undefined>;
}): SceneConfig => {
const { chatConfig } = useChatAppProps();
const refConnectorId = useRef('');
refConnectorId.current = chatConfig?.auth?.connectorId || '';
return useMemo(() => {
const onAfterResponse = [
response => {
const { data: resCreateConversation } = response;
const { code, data: conversationData } = resCreateConversation;
const { id: conversationId, last_section_id: sectionId } =
conversationData || {};
refChatFunc?.current?.setConversationId(conversationId, sectionId);
return {
...response,
data: {
code,
new_section_id: sectionId,
},
};
},
];
const config = {
url:
IS_OPEN_SOURCE && chatConfig.source === OpenApiSource.ChatFlow
? '/v1/workflow/conversation/create'
: '/v1/conversation/create',
method: 'POST',
hooks: {
onBeforeRequest: [
requestConfig => {
const botId = requestConfig.data.bot_id;
return {
...requestConfig,
data: { bot_id: botId, connector_id: refConnectorId.current },
};
},
],
onErrorResponse: onAfterResponse,
onAfterResponse,
},
};
return config;
}, []);
};

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 { useMemo } from 'react';
import { type SceneConfig } from '@coze-common/chat-core';
export const useClearMessageContextAdapter = (): SceneConfig =>
useMemo(() => {
const onAfterResponse = [
response => {
const { data } = response;
const { code, data: res } = data;
return {
...response,
data: {
code,
new_section_id: res.id,
},
};
},
];
return {
url: '/v1/conversations/:conversation_id/clear',
hooks: {
onBeforeRequest: [
requestConfig => {
const conversationId = requestConfig.data.conversation_id;
const url = `/v1/conversations/${conversationId}/clear`;
return {
...requestConfig,
url,
data: { conversation_id: conversationId },
};
},
],
onErrorResponse: onAfterResponse,
onAfterResponse,
},
};
}, []);

View File

@@ -0,0 +1,93 @@
/*
* 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 { useMemo } from 'react';
import axios from 'axios';
import { type RequestManagerOptions } from '@coze-common/chat-core';
import { isAuthError } from '@/util/error';
import { useChatAppProps } from '@/components/studio-open-chat/store';
export const useCommonOnBeforeRequestHooks =
(): Required<RequestManagerOptions>['hooks']['onBeforeRequest'] => {
const { debug } = useChatAppProps();
return [
// 去除无用的头部
requestConfig => {
requestConfig.headers.delete('x-requested-with');
Object.keys(debug?.cozeApiRequestHeader || {}).forEach(key => {
requestConfig.headers.set(
key,
debug?.cozeApiRequestHeader?.[key] || '',
);
});
return requestConfig;
},
];
};
const handleCommonError = async (
response,
refreshToken?: () => Promise<string>,
) => {
const { code } = response?.response?.data || {};
let responseOut = response;
if (isAuthError(code)) {
const token = await refreshToken?.();
if (token) {
const config = { ...response.config };
config.headers = { ...config.headers };
config.headers.Authorization = `Bearer ${token}`;
responseOut = await axios.request(config);
}
}
return responseOut;
};
export const useCommonErrorResponseHooks = (
refreshToken?: () => Promise<string>,
): Required<RequestManagerOptions>['hooks']['onErrorResponse'] =>
useMemo(
() => [async response => handleCommonError(response, refreshToken)],
[refreshToken],
);
export const useCommonOnAfterResponseHooks = (
refreshToken?: () => Promise<string>,
): Required<RequestManagerOptions>['hooks']['onAfterResponse'] =>
useMemo(
() => [
// 用户登录权限判断
async response => handleCommonError(response, refreshToken),
// 用户Url恢复
response => {
if (
response.config.url &&
/^\/v1\/conversations\/[^\\]+\/clear$/.test(response.config.url)
) {
return {
...response,
config: {
...response.config,
url: '/v1/conversations/:conversation_id/clear',
},
};
}
return response;
},
],
[refreshToken],
);

View File

@@ -0,0 +1,128 @@
/*
* 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 { useCallback, useMemo } from 'react';
import { type SceneConfig } from '@coze-common/chat-core';
import { type MixInitResponse } from '@coze-common/chat-area';
import { useChatCozeSdk } from '../context';
import { useChatAppProps } from '../../../store';
import { messageConverterToCoze } from './message';
type ChatMessageList = MixInitResponse['messageList'];
export const useMessageList = (): SceneConfig => {
const getMessageListByPairs = useGetMessageListByPairs();
const { chatConfig } = useChatAppProps();
const { bot_id: botId } = chatConfig || {};
const { refMessageListLeft } = useChatCozeSdk();
return useMemo(() => {
const onAfterResponse = [
response => {
const { data } = response;
const conversationId = response.config?.params?.conversation_id;
const lastMessageList =
(refMessageListLeft?.current?.[conversationId] as ChatMessageList) ||
[];
const lastAnswerChatId =
lastMessageList[lastMessageList.length - 1]?.reply_id;
if (lastAnswerChatId) {
if (
data.data?.[0].type === 'question' &&
!data.message_list?.[0].chatId
) {
data.data[0].chat_id = lastAnswerChatId;
}
}
const dataForCoze = messageConverterToCoze.convertMessageListResponse(
data,
botId,
);
return {
...response,
data: {
...dataForCoze,
message_list: getMessageListByPairs(
conversationId,
dataForCoze.message_list,
),
},
};
},
];
return {
url: '/v1/conversation/message/list',
hooks: {
onBeforeRequest: [
requestConfig => {
const conversationId = requestConfig.data.conversation_id;
const data = {
after_id: requestConfig.data.cursor,
limit: requestConfig.data.count,
};
return {
...requestConfig,
data,
params: {
conversation_id: conversationId,
},
};
},
],
onErrorResponse: onAfterResponse,
onAfterResponse,
},
};
}, [botId]);
};
// 接口返回的数据,并能保证 问题、回答 成对返回,因此需要将多返回的 回答 保存下来,等下次请求数据中的第一条数据是同一个 对话的时候,拼接上去。
export const useGetMessageListByPairs = () => {
const { refMessageListLeft } = useChatCozeSdk();
return useCallback(
(conversationId: string, messageList: ChatMessageList = []) => {
const messageListLeft: ChatMessageList = []; // 需要留下来的
const messageListResponse: ChatMessageList = []; // 需要返回给前端的
for (let i = 0; i < messageList.length; i++) {
if (messageList[i].type !== 'question') {
messageListLeft.push(messageList[i]);
} else {
messageListResponse.push(...messageListLeft);
messageListLeft.splice(0, messageListLeft.length);
messageListResponse.push(messageList[i]);
}
}
const lastMessageList =
(refMessageListLeft?.current?.[conversationId] as ChatMessageList) ||
[];
// 将上次遗留的数据,拼接上去
if (lastMessageList.length) {
if (lastMessageList[0]?.reply_id === messageListResponse[0]?.reply_id) {
messageListResponse.unshift(...lastMessageList);
}
}
// 重置本次遗留的数据
if (refMessageListLeft?.current) {
refMessageListLeft.current[conversationId] = messageListLeft;
}
return messageListResponse;
},
[],
);
};

Some files were not shown because too many files have changed in this diff Show More