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:
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
]);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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 => ({});
|
||||
@@ -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 => ({});
|
||||
@@ -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),
|
||||
});
|
||||
@@ -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?.();
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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 => ({});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' />
|
||||
@@ -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: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
.float-open-conversations-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
}
|
||||
@@ -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} />
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.unfocus-text {
|
||||
color: rgba(48, 68, 112, 38%);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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';
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
}*/
|
||||
});
|
||||
@@ -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),
|
||||
});
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
@@ -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 => ({});
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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' />
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}, []);
|
||||
@@ -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;
|
||||
}, []);
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
@@ -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],
|
||||
);
|
||||
@@ -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
Reference in New Issue
Block a user