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

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

View File

@@ -33,6 +33,12 @@ const mergedConfig = defineConfig({
secure: false,
changeOrigin: true,
},
{
context: ['/v1'],
target: API_PROXY_TARGET,
secure: false,
changeOrigin: true,
},
],
},
html: {

View File

@@ -21,9 +21,9 @@ import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginLess } from '@rsbuild/plugin-less';
import { type RsbuildConfig, mergeRsbuildConfig } from '@rsbuild/core';
import { SemiRspackPlugin } from '@douyinfe/semi-rspack-plugin';
import { PkgRootWebpackPlugin } from '@coze-arch/pkg-root-webpack-plugin';
import { GLOBAL_ENVS } from '@coze-arch/bot-env';
import { SemiRspackPlugin } from '@douyinfe/semi-rspack-plugin';
const getDefine = () => {
const define = {};

View File

@@ -6,6 +6,7 @@
"exports": {
".": "./src/index.js",
"./coze": "./src/coze.js",
"./util": "./src/util.js",
"./design-token": "./src/design-token.ts"
},
"main": "src/index.js",

View File

@@ -0,0 +1,282 @@
/*
* 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.
*/
const plugin = require('tailwindcss/plugin');
const lightModeVariables = require('./light');
const darkModeVariables = require('./dark');
// 用于生成 CSS 变量的帮助函数
function generateCssVariables(variables, theme) {
return Object.entries(variables).reduce((acc, [key, value]) => {
acc[`--${key}`] = theme ? theme(value) : value;
return acc;
}, {});
}
// 样式语义化
function generateSemanticVariables(semantics, theme, property) {
return Object.entries(semantics).map(([key, value]) => ({
[`.${key}`]: {
[property]: theme(value),
},
}));
}
const semanticForeground = {
/* Theme */
'coz-fg-hglt-plus': 'colors.foreground.5',
'coz-fg-hglt-plus-dim': 'colors.foreground.5',
'coz-fg-hglt': 'colors.brand.5',
'coz-fg-hglt-dim': 'colors.brand.3',
'coz-fg-plus': 'colors.foreground.4',
'coz-fg': 'colors.foreground.3',
'coz-fg-primary': 'colors.foreground.3',
'coz-fg-secondary': 'colors.foreground.2',
'coz-fg-dim': 'colors.foreground.1',
'coz-fg-white': 'colors.foreground.7',
'coz-fg-white-dim': 'colors.foreground.white',
'coz-fg-hglt-ai': 'colors.purple.5',
'coz-fg-hglt-ai-dim': 'colors.purple.3',
/* Functional Color */
'coz-fg-hglt-red': 'colors.red.5',
'coz-fg-hglt-red-dim': 'colors.red.3',
'coz-fg-hglt-yellow': 'colors.yellow.5',
'coz-fg-hglt-yellow-dim': 'colors.yellow.3',
'coz-fg-hglt-green': 'colors.green.5',
'coz-fg-hglt-green-dim': 'colors.green.3',
/* Chart, Tag Only */
'coz-fg-color-orange': 'colors.yellow.5',
'coz-fg-color-orange-dim': 'colors.yellow.3',
'coz-fg-color-emerald': 'colors.green.5',
'coz-fg-color-emerald-dim': 'colors.green.3',
'coz-fg-color-cyan': 'colors.cyan.50',
'coz-fg-color-cyan-dim': 'colors.cyan.30',
'coz-fg-color-blue': 'colors.blue.50',
'coz-fg-color-blue-dim': 'colors.blue.30',
'coz-fg-color-purple': 'colors.purple.50',
'coz-fg-color-purple-dim': 'colors.purple.30',
'coz-fg-color-magenta': 'colors.magenta.50',
'coz-fg-color-magenta-dim': 'colors.magenta.3',
'coz-fg-color-yellow': 'colors.yellow.50',
'coz-fg-color-yellow-dim': 'colors.yellow.30',
/* Code Only */
'coz-fg-hglt-orange': 'colors.orange.5',
'coz-fg-hglt-orange-dim': 'colors.orange.3',
'coz-fg-hglt-emerald': 'colors.emerald.5',
'coz-fg-hglt-emerald-dim': 'colors.emerald.3',
'coz-fg-hglt-cyan': 'colors.cyan.5',
'coz-fg-hglt-cyan-dim': 'colors.cyan.3',
'coz-fg-hglt-blue': 'colors.blue.5',
'coz-fg-hglt-blue-dim': 'colors.blue.3',
'coz-fg-hglt-purple': 'colors.purple.5',
'coz-fg-hglt-purple-dim': 'colors.purple.3',
'coz-fg-hglt-magenta': 'colors.magenta.5',
'coz-fg-hglt-magenta-dim': 'colors.magenta.3',
/* branding Only */
'coz-fg-color-brand': 'colors.brand.50',
'coz-fg-color-brand-dim': 'colors.brand.30',
'coz-fg-color-alternative': 'colors.alternative.50',
'coz-fg-color-alternative-dim': 'colors.alternative.30',
};
const semanticMiddleground = {
/* Theme */
'coz-mg-hglt-plus-pressed': 'colors.brand.7',
'coz-mg-hglt-plus-hovered': 'colors.brand.6',
'coz-mg-hglt-plus': 'colors.brand.5',
'coz-mg-hglt-plus-dim': 'colors.brand.3',
'coz-mg-hglt-secondary-pressed': 'colors.brand.2',
'coz-mg-hglt-secondary-hovered': 'colors.brand.1',
'coz-mg-hglt-secondary': 'colors.brand.0',
'coz-mg-hglt-secondary-red': 'colors.red.0',
'coz-mg-hglt-secondary-yellow': 'colors.yellow.0',
'coz-mg-hglt-secondary-green': 'colors.green.0',
'coz-mg-plus-pressed': 'colors.background.8',
'coz-mg-plus-hovered': 'colors.background.7',
'coz-mg-plus': 'colors.background.6',
'coz-mg-hglt-pressed': 'colors.brand.3',
'coz-mg-hglt-hovered': 'colors.brand.2',
'coz-mg-hglt-plus-ai-pressed': 'colors.purple.7',
'coz-mg-hglt-plus-ai-hovered': 'colors.purple.6',
'coz-mg-hglt-plus-ai': 'colors.purple.5',
'coz-mg-hglt-plus-ai-dim': 'colors.purple.3',
'coz-mg-hglt': 'colors.brand.1',
'coz-mg-hglt-ai-pressed': 'colors.purple.3',
'coz-mg-hglt-ai-hovered': 'colors.purple.2',
'coz-mg-hglt-ai': 'colors.purple.1',
/* Functional Color */
'coz-mg-hglt-plus-red-pressed': 'colors.red.7',
'coz-mg-hglt-plus-red-hovered': 'colors.red.6',
'coz-mg-hglt-plus-red': 'colors.red.5',
'coz-mg-hglt-plus-red-dim': 'colors.red.3',
'coz-mg-hglt-plus-yellow-pressed': 'colors.yellow.7',
'coz-mg-hglt-plus-yellow-hovered': 'colors.yellow.6',
'coz-mg-hglt-plus-yellow': 'colors.yellow.5',
'coz-mg-hglt-plus-yellow-dim': 'colors.yellow.3',
'coz-mg-hglt-plus-green-pressed': 'colors.green.7',
'coz-mg-hglt-plus-green-hovered': 'colors.green.6',
'coz-mg-hglt-plus-green': 'colors.green.5',
'coz-mg-hglt-plus-green-dim': 'colors.green.3',
'coz-mg-hglt-red-pressed': 'colors.red.3',
'coz-mg-hglt-red-hovered': 'colors.red.2',
'coz-mg-hglt-red': 'colors.red.1',
'coz-mg-hglt-yellow-pressed': 'colors.yellow.3',
'coz-mg-hglt-yellow-hovered': 'colors.yellow.2',
'coz-mg-hglt-yellow': 'colors.yellow.1',
'coz-mg-hglt-green-pressed': 'colors.green.3',
'coz-mg-hglt-green-hovered': 'colors.green.2',
'coz-mg-hglt-green': 'colors.green.1',
/* Card, Tag, Avatar Only */
'coz-mg-color-plus-orange': 'colors.yellow.5',
'coz-mg-color-plus-emerald': 'colors.green.5',
'coz-mg-color-plus-cyan': 'colors.cyan.50',
'coz-mg-color-plus-blue': 'colors.blue.50',
'coz-mg-color-plus-purple': 'colors.purple.50',
'coz-mg-color-plus-magenta': 'colors.magenta.50',
'coz-mg-color-plus-yellow': 'colors.yellow.50',
'coz-mg-color-orange-pressed': 'colors.yellow.3',
'coz-mg-color-orange-hovered': 'colors.yellow.2',
'coz-mg-color-orange': 'colors.yellow.1',
'coz-mg-color-emerald-pressed': 'colors.green.3',
'coz-mg-color-emerald-hovered': 'colors.green.2',
'coz-mg-color-emerald': 'colors.green.1',
'coz-mg-color-cyan-pressed': 'colors.cyan.30',
'coz-mg-color-cyan-hovered': 'colors.cyan.20',
'coz-mg-color-cyan': 'colors.cyan.10',
'coz-mg-color-blue-pressed': 'colors.blue.30',
'coz-mg-color-blue-hovered': 'colors.blue.20',
'coz-mg-color-blue': 'colors.blue.10',
'coz-mg-color-purple-pressed': 'colors.purple.30',
'coz-mg-color-purple-hovered': 'colors.purple.20',
'coz-mg-color-purple': 'colors.purple.10',
'coz-mg-color-magenta-pressed': 'colors.magenta.30',
'coz-mg-color-magenta-hovered': 'colors.magenta.20',
'coz-mg-color-magenta': 'colors.magenta.10',
'coz-mg-primary-pressed': 'colors.background.7',
'coz-mg-primary-hovered': 'colors.background.6',
'coz-mg-primary': 'colors.background.5',
'coz-mg-secondary-pressed': 'colors.background.6',
'coz-mg-secondary-hovered': 'colors.background.5',
'coz-mg-secondary': 'colors.background.4',
'coz-mg': 'colors.background.4',
'coz-mg-mask': 'colors.mask.5',
'coz-mg-table-fixed-hovered': 'colors.background.0',
'coz-mg-card-pressed': 'colors.background.3',
'coz-mg-card-hovered': 'colors.background.3',
'coz-mg-card': 'colors.background.3',
/** brand */
'coz-mg-color-plus-brand': 'colors.brand.50',
};
const semanticBackground = {
'coz-bg-max': 'colors.background.3',
'coz-bg-plus': 'colors.background.2',
'coz-bg-primary': 'colors.background.1',
'coz-bg': 'colors.background.1',
'coz-bg-secondary': 'colors.background.0',
};
const semanticShadow = {
'coz-shadow': 'boxShadow.normal',
'coz-shadow-large': 'boxShadow.large',
'coz-shadow-default': 'boxShadow.normal',
'coz-shadow-small': 'boxShadow.small',
};
// Add button rounded definitions
const buttonRounded = {
'coz-btn-rounded-large': 'btnBorderRadius.large',
'coz-btn-rounded-normal': 'btnBorderRadius.normal',
'coz-btn-rounded-small': 'btnBorderRadius.small',
'coz-btn-rounded-mini': 'btnBorderRadius.mini',
};
const inputRounded = {
'coz-input-rounded-large': 'inputBorderRadius.large',
'coz-input-rounded-normal': 'inputBorderRadius.normal',
'coz-input-rounded-small': 'inputBorderRadius.small',
};
const inputHeight = {
'coz-input-height-large': 'inputHeight.large',
'coz-input-height-normal': 'inputHeight.normal',
'coz-input-height-small': 'inputHeight.small',
};
const semanticStroke = {
'coz-stroke-hglt': 'colors.brand.5',
'coz-stroke-plus': 'colors.stroke.6',
'coz-stroke-primary': 'colors.stroke.5',
'coz-stroke-hglt-red': 'colors.red.5',
'coz-stroke-hglt-yellow': 'colors.yellow.5',
'coz-stroke-hglt-green': 'colors.green.5',
'coz-stroke-color-orange': 'colors.yellow.5',
'coz-stroke-color-emerald': 'colors.green.5',
'coz-stroke-color-cyan': 'colors.cyan.50',
'coz-stroke-color-blue': 'colors.blue.50',
'coz-stroke-color-purple': 'colors.purple.50',
'coz-stroke-color-magenta': 'colors.magenta.50',
'coz-stroke-color-yellow': 'colors.yellow.50',
'coz-stroke-color-brand': 'colors.brand.50',
'coz-stroke-opaque': 'colors.stroke.opaque',
'coz-stroke-max': 'colors.stroke.max',
};
function genTailwindPlugin(defaultCls, darkCls) {
return plugin(function ({ addBase, addUtilities, theme }) {
addBase({
[defaultCls]: generateCssVariables(lightModeVariables),
[darkCls]: generateCssVariables(darkModeVariables),
});
addBase({
[defaultCls]: {
...generateCssVariables(semanticForeground, theme),
...generateCssVariables(semanticMiddleground, theme),
...generateCssVariables(semanticBackground, theme),
...generateCssVariables(semanticStroke, theme),
...generateCssVariables(semanticShadow, theme),
...generateCssVariables(buttonRounded, theme),
...generateCssVariables(inputRounded, theme),
...generateCssVariables(inputHeight, theme),
},
});
addUtilities([
...generateSemanticVariables(semanticForeground, theme, 'color'),
...generateSemanticVariables(
semanticMiddleground,
theme,
'background-color',
),
...generateSemanticVariables(
semanticBackground,
theme,
'background-color',
),
...generateSemanticVariables(semanticStroke, theme, 'border-color'),
...generateSemanticVariables(semanticShadow, theme, 'box-shadow'),
...generateSemanticVariables(buttonRounded, theme, 'border-radius'),
...generateSemanticVariables(inputRounded, theme, 'border-radius'),
...generateSemanticVariables(inputHeight, theme, 'height'),
]);
});
}
module.exports = {
genTailwindPlugin,
};

View File

@@ -129,10 +129,8 @@ export const BotHeader: React.FC<BotHeaderProps> = props => {
editBotInfoFn={editBotInfoFn}
deployButton={props.deployButton}
/>
{/** mode selector */}
{diffTask || IS_OPEN_SOURCE ? null : (
<ModeSelect optionList={props.modeOptionList} />
)}
{/** 模式选择器 */}
{diffTask ? null : <ModeSelect optionList={props.modeOptionList} />}
</div>
{/* 2. Middle bot menu area - offline */}

View File

@@ -45,4 +45,6 @@ function SwitchWithDesc({
);
}
export const FormSwitch = withField(SwitchWithDesc);
export const FormSwitch = withField(SwitchWithDesc) as ReturnType<
typeof withField
>;

View File

@@ -7,6 +7,7 @@
"maintainers": [],
"exports": {
".": "./src/index.ts",
"./configs": "./src/configs.ts",
"./build": "./scripts/build.ts",
"./typings": "./src/typings.d.ts",
"./runtime": "./src/runtime/index.ts"
@@ -14,6 +15,9 @@
"main": "src/index.ts",
"typesVersions": {
"*": {
"configs": [
"./src/configs.ts"
],
"build": [
"./scripts/build.ts"
],

View File

@@ -3769,5 +3769,29 @@
"workspace_develop": "Development",
"workspace_develop_search_project": "Search for projects",
"workspace_library_search": "Search resources",
"workspace_no_permission_access": "No permission to access this workspace."
"workspace_no_permission_access": "No permission to access this workspace.",
"web_sdk_add_new_conversation": "Create a new conversation",
"store_start_new_chat": "Add chat",
"sendFailed": "Send failed",
"chat_voice_input_need_focus": "Click to activate input",
"web_sdk_create_conversation": "A new session has been created",
"web_sdk_conversation_history": "conversation history",
"web_sdk_conversation_default_name": "Newly created session",
"web_sdk_delete": "delete",
"web_sdk_delete_conversation": "Delete session",
"web_sdk_rename_conversation": "rename session",
"web_sdk_confirm": "OK",
"web_sdk_cancel": "cancel",
"web_sdk_open_conversations": "Expand Sidebar",
"unbind_notification": "Notification: Agent has been unbinded.",
"profile_history_today": "Today",
"log_pay_wall_date_filter_30_days": "Past 30 days",
"web_sdk_past": "past",
"404_title": "Sorry, this page doesn't exist",
"404_content": "Please check your link or try again",
"overview_bi_assistant_system_error": "System error. Please try again later.",
"web_sdk_retry_notification": "Please try again later.",
"chatInputPlaceholder": "Send message",
"web_sdk_official_banner": "Powered by {docs_link}. AI-generated content for reference only.",
"web_sdk_official_banner_link": "coze"
}

View File

@@ -3817,5 +3817,29 @@
"workspace_develop": "项目开发",
"workspace_develop_search_project": "搜索项目",
"workspace_library_search": "搜索资源",
"workspace_no_permission_access": "无法查看空间"
"workspace_no_permission_access": "无法查看空间",
"web_sdk_add_new_conversation": "创建新会话",
"store_start_new_chat": "新增对话",
"sendFailed": "发送失败",
"chat_voice_input_need_focus": "点击当前对话区激活输入框",
"web_sdk_create_conversation": "已创建新会话",
"web_sdk_conversation_history": "会话历史",
"web_sdk_conversation_default_name": "新创建的会话",
"web_sdk_delete": "删除",
"web_sdk_delete_conversation": "删除会话",
"web_sdk_rename_conversation": "重命名会话",
"web_sdk_confirm": "确定",
"web_sdk_cancel": "取消",
"web_sdk_open_conversations": "展开侧栏",
"unbind_notification": "提示:智能体已经被解绑",
"profile_history_today": "今天",
"log_pay_wall_date_filter_30_days": "过去30天",
"web_sdk_past": "过往",
"404_title": "抱歉,该页面不存在",
"404_content": "请检查链接或重试",
"overview_bi_assistant_system_error": "系统错误,请稍后再试",
"web_sdk_retry_notification": "请稍后重试。",
"chatInputPlaceholder": "发送消息",
"web_sdk_official_banner": "由 {docs_link} 提供支持AI生成仅供参考",
"web_sdk_official_banner_link": "扣子"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { memo, useMemo } from 'react';
import { isEqual, isFunction, omitBy } from 'lodash-es';
import { extractChatflowMessage } from './utils';
import { type ChatflowNodeData, type RenderNodeEntryProps } from './type';
import { QuestionNodeRender } from './question-node-render';
import { InputNodeRender } from './input-node-render';
const BaseComponent: React.FC<RenderNodeEntryProps> = ({
message,
...restProps
}) => {
const chatflowNodeData: ChatflowNodeData | undefined = useMemo(
() => extractChatflowMessage(message),
[message],
);
if (!chatflowNodeData) {
return null;
}
if (chatflowNodeData.card_type === 'INPUT') {
return (
<InputNodeRender
data={chatflowNodeData}
message={message}
{...restProps}
/>
);
} else if (chatflowNodeData.card_type === 'QUESTION') {
return (
<QuestionNodeRender
data={chatflowNodeData}
message={message}
{...restProps}
/>
);
} else {
return 'content type is not supported';
}
};
export const WorkflowRenderEntry = memo(BaseComponent, (prevProps, nextProps) =>
isEqual(omitBy(prevProps, isFunction), omitBy(nextProps, isFunction)),
);

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { produce } from 'immer';
import {
type IEventCallbacks,
type IMessage,
} from '@coze-common/chat-uikit-shared';
import { I18n } from '@coze-arch/i18n';
import { Button, Input, Space, Typography } from '@coze-arch/coze-design';
import { type ChatflowNodeData } from './type';
import { NodeWrapperUI } from './node-wrapper-ui';
export const InputNodeRender = ({
data,
onCardSendMsg,
readonly,
isDisable,
message,
}: {
data: ChatflowNodeData;
onCardSendMsg?: IEventCallbacks['onCardSendMsg'];
readonly?: boolean;
isDisable?: boolean;
message: IMessage;
}) => {
const [inputData, setInputData] = useState<Record<string, string>>({});
const [hasSend, setHasSend] = useState(false);
const disabled = readonly || isDisable || hasSend;
return (
<NodeWrapperUI>
<Space spacing={12} vertical className="w-full">
{data.input_card_data?.map((item, index) => (
<Space
align="start"
className="w-full"
spacing={6}
vertical
key={item?.name + index}
>
<Typography.Text ellipsis className="text-lg !font-medium">
{item?.name}
</Typography.Text>
<Input
disabled={disabled || hasSend}
value={inputData[item.name]}
onChange={value => {
setInputData(
produce(draft => {
draft[item.name] = value;
}),
);
}}
/>
</Space>
))}
<Button
className="w-full"
disabled={disabled}
onClick={() => {
if (disabled) {
return;
}
setHasSend(true);
onCardSendMsg?.({
message,
extra: {
msg:
data.input_card_data
?.map(item => `${item.name}:${inputData[item.name] || ''}`)
.join('\n') || '',
mentionList: message.sender_id
? [{ id: message.sender_id }]
: [],
},
});
}}
>
{I18n.t('workflow_detail_title_testrun_submit')}
</Button>
</Space>
</NodeWrapperUI>
);
};

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren } from 'react';
export const NodeWrapperUI: React.FC<PropsWithChildren> = ({ children }) => (
<div className="overflow-hidden w-full min-w-[282px] max-w-[546px] p-[16px] coz-bg-primary">
{children}
</div>
);

View File

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

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type IEventCallbacks,
type IMessage,
} from '@coze-common/chat-uikit-shared';
interface RenderNodeBaseProps extends Pick<IEventCallbacks, 'onCardSendMsg'> {
isDisable: boolean | undefined;
readonly: boolean | undefined;
}
export interface RenderNodeEntryProps extends RenderNodeBaseProps {
message: IMessage;
}
export interface ChatflowNodeData {
card_type: 'QUESTION' | 'INPUT';
input_card_data?: {
type: string;
name: string;
}[];
question_card_data?: {
// eslint-disable-next-line @typescript-eslint/naming-convention
Title: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
Options: { name: string }[];
};
}
export interface ChatflowNodeData {
card_type: 'QUESTION' | 'INPUT';
input_card_data?: {
type: string;
name: string;
}[];
question_card_data?: {
// eslint-disable-next-line @typescript-eslint/naming-convention
Title: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
Options: { name: string }[];
};
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type IMessage } from '@coze-common/chat-uikit-shared';
import { safeJSONParse } from '@coze-common/chat-uikit';
import { type ChatflowNodeData } from './type';
export const extractChatflowMessage = (message: IMessage) => {
if (message.content_type === 'card') {
const contentStruct = safeJSONParse(message.content) as {
x_properties: {
workflow_card_info: string;
};
};
const workflowDataStr = contentStruct?.x_properties?.workflow_card_info;
if (workflowDataStr) {
const cardData = safeJSONParse(workflowDataStr) as ChatflowNodeData;
if (cardData?.card_type === 'QUESTION' && cardData?.question_card_data) {
return cardData;
}
if (cardData?.card_type === 'INPUT' && cardData?.input_card_data) {
return cardData;
}
}
}
};

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ContentBoxType } from '@coze-common/chat-uikit-shared';
import {
ContentBox,
type EnhancedContentConfig,
ContentType,
} from '@coze-common/chat-uikit';
import {
PluginScopeContextProvider,
usePluginCustomComponents,
type ComponentTypesMap,
} from '@coze-common/chat-area';
import { WorkflowRenderEntry } from './components';
const defaultEnable = (value?: boolean) => {
if (typeof value === 'undefined') {
return true;
}
return value;
};
export const ChatFlowRender: ComponentTypesMap['contentBox'] = props => {
const customTextMessageInnerTopSlotList = usePluginCustomComponents(
'TextMessageInnerTopSlot',
);
const enhancedContentConfigList: EnhancedContentConfig[] = [
{
rule: ({ contentType, contentConfigs }) => {
const isCardEnable = defaultEnable(
contentConfigs?.[ContentBoxType.CARD]?.enable,
);
return contentType === ContentType.Card && isCardEnable;
},
render: ({ message, eventCallbacks, options }) => {
const { isCardDisabled, readonly } = options;
const { onCardSendMsg } = eventCallbacks ?? {};
return (
<WorkflowRenderEntry
message={message}
onCardSendMsg={onCardSendMsg}
readonly={readonly}
isDisable={isCardDisabled}
/>
);
},
},
];
return (
<ContentBox
enhancedContentConfigList={enhancedContentConfigList}
multimodalTextContentAddonTop={
<>
{customTextMessageInnerTopSlotList.map(
// eslint-disable-next-line @typescript-eslint/naming-convention -- matches the expected naming
({ pluginName, Component }, index) => (
<PluginScopeContextProvider
pluginName={pluginName}
key={pluginName}
>
<Component key={index} message={props.message} />
</PluginScopeContextProvider>
),
)}
</>
}
{...props}
/>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,12 +17,12 @@
import React, { Suspense, lazy, useMemo } from 'react';
import { userStoreService } from '@coze-studio/user-store';
import type { IProject } from '@coze-studio/open-chat';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
import { I18n } from '@coze-arch/i18n';
import { IconCozIllusAdd } from '@coze-arch/coze-design/illustrations';
import { EmptyState } from '@coze-arch/coze-design';
import { CreateEnv } from '@coze-arch/bot-api/workflow_api';
import type { IProject } from '@coze-studio/open-chat';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
import { DISABLED_CONVERSATION } from '../constants';
import { useSkeleton } from './use-skeleton';
@@ -81,8 +81,8 @@ export const ChatHistory: React.FC<ChatHistoryProps> = ({
const chatUserInfo = {
id: userInfo?.user_id_str || '',
name: userInfo?.name || '',
avatar: userInfo?.avatar_url || '',
nickname: userInfo?.name || '',
url: userInfo?.avatar_url || '',
};
if (

View File

@@ -16,9 +16,6 @@
import { I18n } from '@coze-arch/i18n';
// Default session unique_id
export const DEFAULT_UNIQUE_ID = '0';
export const DEFAULT_CONVERSATION_NAME = 'Default';
export const MAX_LIMIT = 1000;

View File

@@ -16,6 +16,7 @@
import React, { useMemo, useState } from 'react';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
import { I18n } from '@coze-arch/i18n';
import { IconCozChat } from '@coze-arch/coze-design/icons';
import { Modal, Select, Typography, Toast } from '@coze-arch/coze-design';
@@ -24,9 +25,8 @@ import {
type ProjectConversation,
} from '@coze-arch/bot-api/workflow_api';
import { workflowApi } from '@coze-arch/bot-api';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
import { DEFAULT_UNIQUE_ID, DEFAULT_CONVERSATION_NAME } from '../../constants';
import { DEFAULT_CONVERSATION_NAME } from '../../constants';
import s from './index.module.less';
@@ -154,7 +154,7 @@ export const useDeleteChat = ({
style={{ width: '50%' }}
dropdownStyle={{ width: 220 }}
size="small"
defaultValue={DEFAULT_UNIQUE_ID}
defaultValue={optionList[0]?.value}
optionList={optionList}
onChange={value => {
const selectItem = staticList.find(

View File

@@ -30,7 +30,7 @@ import { type ProjectConversation } from '@coze-arch/bot-api/workflow_api';
import { TitleWithTooltip } from '../title-with-tooltip';
import commonStyles from '../conversation-content/index.module.less';
import { EditInput } from '../conversation-content/edit-input';
import { DEFAULT_UNIQUE_ID, type ErrorCode } from '../constants';
import { type ErrorCode } from '../constants';
import s from './index.module.less';
@@ -140,9 +140,7 @@ export const StaticChatList = ({
{item.conversation_name}
</Text>
)}
{editingUniqueId === item.unique_id ||
item.unique_id === DEFAULT_UNIQUE_ID ||
!canEdit ? null : (
{editingUniqueId === item.unique_id || !canEdit ? null : (
<div className={commonStyles.icons}>
<IconButton
size="small"

View File

@@ -41,10 +41,10 @@ import {
usePrimarySidebarStore,
} from '@coze-project-ide/biz-components';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { ResourceCopyScene } from '@coze-arch/bot-api/plugin_develop';
import { workflowApi } from '@coze-arch/bot-api';
import { Toast } from '@coze-arch/coze-design';
import { WORKFLOW_SUB_TYPE_ICON_MAP } from '@/constants';
import { WorkflowTooltip } from '@/components';
@@ -233,33 +233,29 @@ export const useWorkflowResource = (): UseWorkflowResourceReturn => {
subType: WorkflowMode.Workflow,
tooltip: <WorkflowTooltip flowMode={WorkflowMode.Workflow} />,
},
// The open-source version does not currently support conversation streaming
IS_OPEN_SOURCE
? null
: {
icon: WORKFLOW_SUB_TYPE_ICON_MAP[WorkflowMode.ChatFlow],
label: I18n.t('project_resource_sidebar_create_new_resource', {
resource: I18n.t('wf_chatflow_76'),
}),
subType: WorkflowMode.ChatFlow,
tooltip: <WorkflowTooltip flowMode={WorkflowMode.ChatFlow} />,
},
{
icon: WORKFLOW_SUB_TYPE_ICON_MAP[WorkflowMode.ChatFlow],
label: I18n.t('project_resource_sidebar_create_new_resource', {
resource: I18n.t('wf_chatflow_76'),
}),
subType: WorkflowMode.ChatFlow,
tooltip: <WorkflowTooltip flowMode={WorkflowMode.ChatFlow} />,
},
].filter(Boolean) as ResourceFolderCozeProps['createResourceConfig'],
[],
);
const iconRender: ResourceFolderCozeProps['iconRender'] = useMemo(
() =>
({ resource }) =>
(
<>
{
WORKFLOW_SUB_TYPE_ICON_MAP[
resource.res_sub_type || WorkflowMode.Workflow
]
}
</>
),
({ resource }) => (
<>
{
WORKFLOW_SUB_TYPE_ICON_MAP[
resource.res_sub_type || WorkflowMode.Workflow
]
}
</>
),
[],
);

View File

@@ -36,7 +36,7 @@
"@coze-studio/publish-manage-hooks": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@coze-workflow/base": "workspace:*",
"@coze/api": "1.1.0-beta.4",
"@coze/api": "1.3.5",
"ahooks": "^3.7.8",
"classnames": "^2.3.2",
"dayjs": "^1.11.7",

View File

@@ -90,22 +90,19 @@ export const Configuration = () => {
onClick={handleSwitchExpand}
/>
</div>
{/* will support soon */}
{IS_OPEN_SOURCE ? null : (
<div
className={classnames(
styles.item,
compareURI(context?.uri, SESSION_CONFIG_URI) && styles.activate,
)}
onClick={handleOpenSession}
>
<IconCozChatSetting
className="coz-fg-plus"
style={{ marginRight: 4 }}
/>
{I18n.t('wf_chatflow_101')}
</div>
)}
<div
className={classnames(
styles.item,
compareURI(context?.uri, SESSION_CONFIG_URI) && styles.activate,
)}
onClick={handleOpenSession}
>
<IconCozChatSetting
className="coz-fg-plus"
style={{ marginRight: 4 }}
/>
{I18n.t('wf_chatflow_101')}
</div>
<div
className={classnames(
styles.item,

View File

@@ -73,8 +73,7 @@ const ProjectIDE: React.FC<ProjectIDEProps> = memo(
() => ({
view: {
widgetRegistries: [
// will support soon
...(IS_OPEN_SOURCE ? [] : [ConversationRegistry]),
ConversationRegistry,
WorkflowWidgetRegistry,
DatabaseWidgetRegistry,
KnowledgeWidgetRegistry,

View File

@@ -0,0 +1,6 @@
CHAT_APP_CHATFLOW_COZE_APP_ID=""
CHAT_APP_CHATFLOW_COZE_WORKFLOW_ID=""
CHAT_APP_INDEX_COZE_BOT_ID=""
CHAT_APP_COZE_TOKEN=""
CHAT_APP_COZE_BOT_USER_URL=""

View File

@@ -0,0 +1,6 @@
CHAT_APP_CHATFLOW_COZE_APP_ID="7542447949096157184"
CHAT_APP_CHATFLOW_COZE_WORKFLOW_ID="7542447968176046080"
CHAT_APP_INDEX_COZE_BOT_ID=""
CHAT_APP_COZE_TOKEN="pat_aec5209b90cdac8883547dff09fe0000e6c1b296347c977c16cdb70f094a55ca"
CHAT_APP_COZE_BOT_USER_URL=""

View File

@@ -0,0 +1,4 @@
inhouse
libs
dist_ignore
.env.local*

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
rules: {
'order/properties-order': null,
},
});

View File

@@ -0,0 +1,9 @@
reviewers:
- sunkuo
- gaoyuanhan.duty
- gaoding.devingao
- shenxiaojie.316
- zhangyingdong
- shanrenkai
- yangyu.1
approvals_required: 1

View File

@@ -0,0 +1,3 @@
# @flow-platform/chat-app-sdk
https://bytedance.larkoffice.com/wiki/IdTkw7Kd5iahWLkv6FKcIo53nBe

View File

@@ -0,0 +1,16 @@
{
"operationSettings": [
{
"operationName": "build",
"outputFolderNames": ["dist", "inhouse", "libs"]
},
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist_ignore"]
}
]
}

View File

@@ -0,0 +1,6 @@
{
"codecov": {
"coverage": 30,
"incrementCoverage": 60
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {
'@coze-arch/max-line-per-function': [
'error',
{
max: 300,
},
],
'no-restricted-imports': 'off',
},
ignores: ['**/inhouse', '**/libs', '**/dist_*'],
});

View File

@@ -0,0 +1,126 @@
{
"name": "@coze-studio/chat-app-sdk",
"version": "1.2.0-beta.17",
"description": "Coze Web ChatApp SDK",
"license": "Apache-2.0",
"author": "yangyu.1@bytedance.com",
"maintainers": [
"liuyuhang.0@bytedance.com",
"yangyu.1@bytedance.com"
],
"sideEffects": [
"**/*.css",
"**/*.less",
"**/*.scss"
],
"exports": {
".": "./src/index.ts",
"./rspack": "./rspack-config/export.ts"
},
"main": "src/index.ts",
"unpkg": true,
"types": "./src/index.ts",
"typesVersions": {
"*": {
"rspack": [
"./rspack-config/export.ts"
]
}
},
"files": [
"inhouse",
"libs",
"README.md"
],
"scripts": {
"analyze": "ANALYZE_MODE=true pnpm build:inhouse:cn --analyze",
"analyze:perf": "PERFSEE=true npm run build:release:cn",
"bam": "bam update",
"build": "IS_OPEN_SOURCE=true rm -rf dist_ignore inhouse libs && concurrently \"npm:build:*\"",
"build:inhouse:boe": "CUSTOM_VERSION=inhouse BUILD_TYPE=offline REGION=cn npm run rsbuild",
"build:inhouse:cn": "CUSTOM_VERSION=inhouse BUILD_TYPE=online REGION=cn npm run rsbuild",
"build:inhouse:sg": "CUSTOM_VERSION=inhouse BUILD_TYPE=online REGION=sg npm run rsbuild",
"build:release:cn": "CUSTOM_VERSION=release BUILD_TYPE=online REGION=cn npm run rsbuild",
"build:release:oversea": "CUSTOM_VERSION=release BUILD_TYPE=online REGION=sg npm run rsbuild",
"build:ts": "tsc -b tsconfig.build.json",
"dev": "IS_OPEN_SOURCE=true npm run dev:cn:rl",
"dev:boe": "CUSTOM_VERSION=inhouse REGION=cn BUILD_TYPE=offline pnpm rsdev",
"dev:cn": "CUSTOM_VERSION=inhouse REGION=cn BUILD_TYPE=online pnpm rsdev",
"dev:cn:rl": "CUSTOM_VERSION=release REGION=cn BUILD_TYPE=online pnpm rsdev",
"dev:sg": "CUSTOM_VERSION=inhouse REGION=sg BUILD_TYPE=online pnpm rsdev",
"dev:sg:rl": "CUSTOM_VERSION=release REGION=sg BUILD_TYPE=online pnpm rsdev",
"lint": "eslint ./ --cache",
"rsbuild": "NODE_ENV=production rspack build -c ./rspack-config/build.config.ts",
"rsdev": "NODE_ENV=development rspack serve -c ./rspack-config/dev.config.ts",
"test": "vitest --run --passWithNoTests",
"test:cov": "vitest run --coverage"
},
"dependencies": {
"@coze-common/assets": "workspace:*",
"classnames": "^2.3.2",
"core-js": "^3.37.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",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-env": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/pkg-root-webpack-plugin": "workspace:*",
"@coze-arch/postcss-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/tailwind-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@coze-studio/bot-env-adapter": "workspace:*",
"@coze-studio/open-chat": "workspace:*",
"@douyinfe/semi-rspack-plugin": "2.61.0",
"@rspack/cli": "0.6.0",
"@rspack/core": "0.6.0",
"@rspack/plugin-react-refresh": "0.6.0",
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/lodash-es": "^4.17.10",
"@types/node": "^18",
"@types/postcss-js": "^4.0.2",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"autoprefixer": "^10.4.16",
"concurrently": "~8.2.2",
"css-loader": "^6.10.0",
"debug": "^4.3.4",
"file-loader": "^6.2.0",
"less": "^4.2.0",
"less-loader": "~11.1.3",
"postcss": "^8.4.32",
"postcss-loader": "^7.3.3",
"react-refresh": "0.14.0",
"react-router-dom": "^6.11.1",
"rspack-plugin-dotenv": "^0.0.3",
"sass": "^1.69.5",
"sass-loader": "^14.1.0",
"style-loader": "^3.3.4",
"tailwindcss": "~3.3.3",
"ts-node": "^10.9.1",
"typescript": "~5.8.2",
"vitest": "~3.0.5",
"webpack": "~5.91.0"
},
"// deps": "debug@^4.3.4 为脚本自动补齐,请勿改动",
"botPublishConfig": {
"main": "dist/index.js"
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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 { configs as GLOBAL_ENVS } from '@coze-studio/bot-env-adapter/configs';
import { openSdkDefineEnvs } from './env';
import { IS_OVERSEA } from './base';
export const getRspackAppDefineEnvs = () => ({
...openSdkDefineEnvs,
/**
* ChatArea 依赖
*/
IS_OVERSEA,
CARD_BUILDER_ENV_STR: JSON.stringify(GLOBAL_ENVS.CARD_BUILDER_ENV_STR),
SAMI_WS_ORIGIN: JSON.stringify(GLOBAL_ENVS.SAMI_WS_ORIGIN),
SAMI_APP_KEY: JSON.stringify(GLOBAL_ENVS.SAMI_APP_KEY),
SAMI_CHAT_WS_URL: JSON.stringify(GLOBAL_ENVS.SAMI_CHAT_WS_URL),
COZE_API_TTS_BASE_URL: JSON.stringify(GLOBAL_ENVS.COZE_API_TTS_BASE_URL),
FEATURE_ENABLE_MSG_DEBUG: false,
APP_ID: '""',
COZE_DOMAIN: JSON.stringify(GLOBAL_ENVS.COZE_DOMAIN),
});

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {
REGION,
BUILD_TYPE,
CUSTOM_VERSION,
NODE_ENV: ENV,
ANALYZE_MODE,
PERFSEE,
IS_OPEN_SOURCE,
} = process.env;
const NODE_ENV = ENV as 'development' | 'production';
const IS_DEV_MODE = NODE_ENV !== 'production';
const IS_BOE = BUILD_TYPE === 'offline';
const IS_RELEASE_VERSION = CUSTOM_VERSION === 'release';
const IS_OVERSEA = REGION !== 'cn';
const IS_ANALYZE_MODE = ANALYZE_MODE === 'true';
const IS_PERFSEE = PERFSEE === 'true';
export {
IS_PERFSEE,
IS_DEV_MODE,
IS_BOE,
IS_RELEASE_VERSION,
IS_OVERSEA,
CUSTOM_VERSION,
NODE_ENV,
REGION,
IS_ANALYZE_MODE,
IS_OPEN_SOURCE,
};
type EnvVar = boolean | string;
export const getEnvConfig = (
config: {
cn: {
boe?: EnvVar;
inhouse?: EnvVar;
release?: EnvVar;
};
sg: {
inhouse: EnvVar;
release: EnvVar;
};
va: {
release: EnvVar;
};
},
defaultVal: EnvVar = '',
// @ts-expect-error -- linter-disable-autofix
): EnvVar => config[REGION]?.[IS_BOE ? 'boe' : CUSTOM_VERSION] ?? defaultVal;

View File

@@ -0,0 +1,166 @@
/*
* 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 path from 'path';
import { DefinePlugin, ProgressPlugin, type Configuration } from '@rspack/core';
import { SemiRspackPlugin } from '@douyinfe/semi-rspack-plugin';
import PkgRootWebpackPlugin from '@coze-arch/pkg-root-webpack-plugin';
import { PREFIX_CLASS } from './semi-css-var-postcss-plugin';
import { cssLoaders, sideEffectsRules, swcTsLoader } from './rules';
import { openSdkUnPkgDirName } from './env';
import { IS_ANALYZE_MODE } from './base';
import { getRspackAppDefineEnvs } from './app';
// eslint-disable-next-line @typescript-eslint/naming-convention -- __dirname
const __rootName = path.resolve(__dirname, '../');
const config: Configuration = {
mode: 'production',
context: __rootName,
optimization: {
splitChunks: false,
...(IS_ANALYZE_MODE
? {
minimize: false,
chunkIds: 'named',
}
: {}),
},
entry: {
main: ['./src/index.ts'],
ui: './src/export-ui/index.ts',
},
experiments: {
css: false,
},
output: {
path: openSdkUnPkgDirName,
filename: pathData =>
pathData.chunk?.name === 'main' ? 'index.js' : '[name].js',
library: {
name: 'CozeWebSDK[name]',
type: 'umd',
},
},
target: ['web'],
resolve: {
tsConfigPath: path.resolve(__rootName, './tsconfig.json'), // https://www.rspack.dev/config/resolve.html#resolvetsconfigpath
alias: {
'@coze-arch/i18n$': path.resolve(
__rootName,
'./node_modules/@coze-arch/i18n/src/raw/index.ts',
),
/**
* swc.env.mode='usage'
*/
'core-js': path.dirname(require.resolve('core-js')),
},
extensions: ['...', '.tsx', '.ts', '.jsx'],
},
module: {
rules: [
...sideEffectsRules,
{
test: /\.svg$/,
issuer: /\.[jt]sx?$/,
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
native: false,
},
},
'file-loader',
],
},
{
test: /\.(png|gif|jpg|jpeg|woff2)$/,
type: 'asset',
},
{
test: /\.less$/,
use: [
...cssLoaders,
{
loader: 'less-loader',
options: {},
},
],
},
{
test: /\.scss$/,
use: [
...cssLoaders,
{
loader: 'sass-loader',
options: {
sassOptions: {
silenceDeprecations: [
'mixed-decls',
'import',
'function-units',
],
},
},
},
],
},
{
test: /\.css$/,
use: cssLoaders,
},
{
test: /\.tsx?$/,
exclude: {
and: [/\/node_modules\//, /^((?!@byted\/mojito-safe-fund).)*$/],
},
use: swcTsLoader,
},
],
},
builtins: {
treeShaking: true,
},
plugins: [
new DefinePlugin(getRspackAppDefineEnvs()),
new ProgressPlugin({}),
new PkgRootWebpackPlugin({}),
new SemiRspackPlugin({
prefixCls: PREFIX_CLASS,
}),
].filter(Boolean) as Configuration['plugins'],
devServer: {
allowedHosts: 'all',
historyApiFallback: true,
hot: true,
},
devtool: false,
};
export default config;

View File

@@ -0,0 +1,199 @@
/*
* 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 path from 'path';
import { DotenvPlugin } from 'rspack-plugin-dotenv';
import refreshPlugin from '@rspack/plugin-react-refresh';
import {
DefinePlugin,
ProgressPlugin,
type Configuration,
HtmlRspackPlugin,
} from '@rspack/core';
import { SemiRspackPlugin } from '@douyinfe/semi-rspack-plugin';
import PkgRootWebpackPlugin from '@coze-arch/pkg-root-webpack-plugin';
import { devCssLoaders, swcTsLoader } from './rules';
import { devDefineEnvs } from './dev';
// eslint-disable-next-line @typescript-eslint/naming-convention -- __dirname
const __rootName = path.resolve(__dirname, '../');
const config: Configuration = {
mode: 'development',
context: __rootName,
entry: {
main: ['./src/dev-app/index.tsx'],
},
experiments: {
css: true,
},
target: ['web'],
resolve: {
tsConfigPath: path.resolve(__rootName, 'tsconfig.json'), // https://www.rspack.dev/config/resolve.html#resolvetsconfigpath
alias: {
'@coze-arch/i18n$': path.resolve(
__rootName,
'./node_modules/@coze-arch/i18n/src/raw/index.ts',
),
/**
* swc.env.mode='usage'
*/
'core-js': path.dirname(require.resolve('core-js')),
},
extensions: ['...', '.tsx', '.ts', '.jsx'],
},
module: {
parser: {
'css/auto': {
namedExports: false,
},
},
generator: {
'css/auto': {
exportsConvention: 'camel-case',
localIdentName: '[hash]-[local]',
},
},
rules: [
{
test: /\.less$/,
use: [
...devCssLoaders,
{
loader: 'less-loader',
options: {},
},
],
type: 'css/auto',
},
{
test: /\.scss$/,
use: [
...devCssLoaders,
{
loader: 'sass-loader',
options: {
sassOptions: {
silenceDeprecations: [
'mixed-decls',
'import',
'function-units',
],
},
},
},
],
type: 'css/auto',
},
{
test: /\.css$/,
use: devCssLoaders,
},
{
test: /\.svg$/,
issuer: /\.[jt]sx?$/,
use: [
{
loader: '@svgr/webpack',
options: {
native: false,
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
},
},
'file-loader',
],
},
{
test: /\.(png|gif|jpg|jpeg|woff2)$/,
use: 'file-loader',
},
{
test: /\.tsx?$/,
exclude: {
and: [/\/node_modules\//, /^((?!@byted\/mojito-safe-fund).)*$/],
},
use: swcTsLoader,
},
],
},
builtins: {
treeShaking: true,
},
plugins: [
new DotenvPlugin({
path: path.resolve(
__rootName,
devDefineEnvs.IS_BOE ? '.env.local.boe' : '.env.local',
),
systemvars: false,
defaults: true,
allowEmptyValues: true,
}),
new DefinePlugin({
...devDefineEnvs,
IS_PROD: !devDefineEnvs.IS_BOE,
}),
new ProgressPlugin({}),
new PkgRootWebpackPlugin({}),
new SemiRspackPlugin({
prefixCls: 'coze-chat-sdk-semi',
}),
new HtmlRspackPlugin(),
new refreshPlugin(),
] as Configuration['plugins'],
stats: false,
devServer: {
allowedHosts: 'all',
compress: false,
historyApiFallback: true,
port: '8081',
hot: true,
proxy: [
{
context: ['/api'],
target: 'http://localhost:8888',
secure: false,
changeOrigin: true,
},
{
context: ['/v1'],
target: 'http://localhost:8888',
secure: false,
changeOrigin: true,
},
{
context: ['/v3'],
target: 'http://localhost:8888',
secure: false,
changeOrigin: true,
},
],
},
};
export default config;

View File

@@ -0,0 +1,21 @@
/*
* 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 DefinePluginOptions } from '@rspack/core';
import { getRspackAppDefineEnvs } from './app';
export const devDefineEnvs: DefinePluginOptions = getRspackAppDefineEnvs();

View File

@@ -0,0 +1,90 @@
/*
* 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 {
IS_RELEASE_VERSION,
IS_DEV_MODE,
NODE_ENV,
REGION,
IS_BOE,
getEnvConfig,
IS_OVERSEA,
IS_OPEN_SOURCE,
} from './base';
export const openSdkDefineEnvs = {
IS_BOE,
IS_DEV_MODE,
REGION: JSON.stringify(REGION),
IS_RELEASE_VERSION,
IS_OVERSEA,
FEATURE_ENABLE_TEA_UG: false,
IS_PROD: !IS_BOE,
IS_OPEN_SOURCE,
};
const getUnPkgDirName = () => {
if (IS_BOE) {
return 'inhouse/boe';
}
let name = '';
if (IS_RELEASE_VERSION) {
switch (REGION) {
case 'sg':
case 'va':
name = 'oversea';
break;
case 'cn':
name = 'cn';
break;
default:
name = '';
}
return `libs/${name}`;
}
return `inhouse/${REGION}`;
};
export const openSdkUnPkgDirName = getUnPkgDirName();
const slardarVaPath = '/maliva';
const slardarSgPath = '/sg';
export const openSdkSlardarRegion = getEnvConfig({
cn: {
boe: '',
inhouse: '',
release: '',
},
sg: {
inhouse: slardarSgPath,
release: slardarSgPath,
},
va: {
release: slardarVaPath,
},
});
console.debug(
'open-sdk',
NODE_ENV,
'\nopenSdkDefineEnvs:',
openSdkDefineEnvs,
'\nopenSdkSlardarRegion:',
openSdkSlardarRegion,
);

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { swcTsLoader, devCssLoaders } from './rules';
export { getRspackAppDefineEnvs } from './app';
export { openSdkSlardarRegion } from './env';
export {
IS_DEV_MODE,
IS_RELEASE_VERSION,
CUSTOM_VERSION,
NODE_ENV,
REGION,
IS_BOE,
} from './base';

View File

@@ -0,0 +1,129 @@
/*
* 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 RuleSetRule } from '@rspack/core';
import semiCssVarPrefixPlugin from './semi-css-var-postcss-plugin';
import { IS_DEV_MODE } from './base';
type UseLoaders = Extract<RuleSetRule['use'], unknown[]>;
export const cssLoaders: UseLoaders = [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: IS_DEV_MODE,
modules: {
auto: true,
exportLocalsConvention: 'camelCase',
localIdentName: !IS_DEV_MODE ? '[hash]' : '[path][name][ext]__[local]',
},
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('tailwindcss')(),
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('autoprefixer')(),
semiCssVarPrefixPlugin(),
],
},
},
},
];
/**
* 已经标记 sideEffects: false无需覆盖 的pkg:
* chat-open
*/
export const sideEffectsRules: RuleSetRule[] = [
{
test: /packages\/components\/bot-icons/,
sideEffects: false,
},
{
test: /packages\/components\/bot-semi/,
sideEffects: false,
},
{
test: /packages\/studio\/chat-area/,
sideEffects: false,
},
{
test: /packages\/studio\/chat-core/,
sideEffects: false,
},
{
test: /packages\/arch\/i18n/,
sideEffects: false,
},
].filter(r => r);
export const swcTsLoader: UseLoaders = [
{
loader: 'builtin:swc-loader',
options: {
sourceMap: IS_DEV_MODE,
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
development: IS_DEV_MODE,
refresh: IS_DEV_MODE,
},
},
},
env: {
mode: 'usage',
coreJs: '3.37.1',
targets: [
'chrome >= 87',
'edge >= 88',
'firefox >= 78',
'safari >= 14',
],
},
},
},
];
export const devCssLoaders: UseLoaders = [
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('tailwindcss')(),
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('autoprefixer')(),
semiCssVarPrefixPlugin(),
],
},
},
},
];

View File

@@ -0,0 +1,90 @@
/*
* 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.
*/
/**
* PostCSS 插件:为 Semi 组件类名和 CSS 变量添加前缀
* 解决 coze-design 里 hardcode 的 .semi-xxx 类名与 prefixCls 不匹配导致样式失效问题
* 兼容多类名、嵌套、伪类、组合选择器等复杂情况
*
* 注意:本插件应在 coze-design 的样式被引入后生效,确保所有 .semi-xxx 都能被正确加前缀
*
* 已添加调试代码,可通过环境变量 DEBUG_SEMI_CSS_VAR_PLUGIN 控制输出
*/
import type { PluginCreator } from 'postcss';
export const PREFIX_CLASS = 'coze-chat-sdk-semi';
const CSS_VAR_PREFIX = `${PREFIX_CLASS}-`;
const SEMI_CLASS_PREFIX = 'semi-';
const CUSTOM_CLASS_PREFIX = `${PREFIX_CLASS}-`;
// 处理选择器,将 .semi-xxx 替换为 .coze-chat-sdk-semi-xxx
function processSelector(selector: string): string {
// 只处理 .semi-xxx不管前面有无其它类名、伪类、组合等
// 例如:.semi-button、.semi-button-primary:hover、.foo .semi-button.bar
// 注意:不要重复加前缀
// 兼容 :is(.semi-button), :not(.semi-button), .semi-button:hover, .semi-button.foo
// 兼容多个选择器用逗号分隔的情况
const replaced = selector.replace(
/\.semi-([a-zA-Z0-9_-]+)/g,
(match, className) => {
// 已经有前缀的不处理
if (match.includes(`.${CUSTOM_CLASS_PREFIX}`)) {
return match;
}
return `.${CUSTOM_CLASS_PREFIX}${className}`;
},
);
return replaced;
}
const semiCssVarPrefixPlugin: PluginCreator<void> = () => ({
postcssPlugin: 'semi-css-var-prefix',
// eslint-disable-next-line @typescript-eslint/naming-convention
Rule(rule) {
// 只要选择器里有 .semi-,就处理
if (rule.selector && rule.selector.includes(`.${SEMI_CLASS_PREFIX}`)) {
rule.selector = processSelector(rule.selector);
}
},
// eslint-disable-next-line @typescript-eslint/naming-convention
Declaration(decl) {
// 处理 CSS 变量定义
if (decl.prop && decl.prop.startsWith('--semi-')) {
decl.prop = decl.prop.replace(/^--semi-/, `--${CSS_VAR_PREFIX}`);
}
// 处理 CSS 变量引用
if (decl.value && decl.value.includes('var(--semi-')) {
decl.value = decl.value.replace(
/var\(--semi-([a-zA-Z0-9_-]+)\)/g,
`var(--${CSS_VAR_PREFIX}$1)`,
);
}
// 处理 rgba(var(--semi-xxx), ...)
if (decl.value && decl.value.includes('rgba(var(--semi-')) {
decl.value = decl.value.replace(
/rgba\(var\(--semi-([a-zA-Z0-9_-]+)\)/g,
`rgba(var(--${CSS_VAR_PREFIX}$1)`,
);
}
},
});
semiCssVarPrefixPlugin.postcss = true;
export default semiCssVarPrefixPlugin;

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, describe, test, vi } from 'vitest';
import { Layout } from '@coze-studio/open-chat/types';
import { type CozeChatOptions } from '@/types/client';
import { AuthClient } from '../auth';
import type * as ClientModule from '..';
vi.hoisted(() => {
// @ts-expect-error -- 将 IS_OVERSEA 提升到最外层
global.IS_OVERSEA = false;
});
vi.mock('@/components/widget', () => ({
default: vi.fn(),
}));
const testBotId = '7313780910216806444';
const config = {
config: {
botId: testBotId,
},
auth: {
type: 'token',
onRefreshToken: () => Promise.resolve('test'),
token: 'Test',
},
componentProps: {
title: '历史学教授',
},
};
const config2: CozeChatOptions = {
config: {
bot_id: testBotId,
},
auth: {
// @ts-expect-error -- 测试兼容逻辑
type: 'token',
onRefreshToken: () => Promise.resolve('test'),
token: 'Test',
},
componentProps: {
title: '历史学教授',
},
};
const config3: CozeChatOptions = {
config: {
bot_id: testBotId,
},
auth: {
onRefreshToken: () => Promise.resolve('test'),
token: 'Test',
},
componentProps: {
title: '历史学教授',
},
};
const config4: CozeChatOptions = {
config: {
bot_id: testBotId,
},
auth: {
// @ts-expect-error -- 测试兼容逻辑
type: 'token',
onRefreshToken: () => Promise.resolve(''),
token: '',
},
componentProps: {
title: '历史学教授',
},
};
describe('client', async () => {
const { WebChatClient } = await vi.importActual<typeof ClientModule>('..');
test('client list', () => {
const client1 = new WebChatClient(config);
const client2 = new WebChatClient({
...config,
el: document.createElement('div'),
});
client1.destroy();
expect(WebChatClient.clients.length).toBe(1);
expect(!WebChatClient.clients.includes(client1)).toBe(true);
client2.destroy();
expect(WebChatClient.clients.length).toBe(0);
expect(!WebChatClient.clients.includes(client2)).toBe(true);
});
test('client mount', () => {
const client = new WebChatClient(config2);
const client2 = new WebChatClient({
...config2,
el: document.createElement('div'),
});
// @ts-expect-error -- ut
expect(!!client.defaultRoot).toBe(true);
// @ts-expect-error -- ut
expect(!!client2.defaultRoot).toBe(false);
});
test('init layout mobile', () => {
vi.mock('react-device-detect', () => ({ isMobileOnly: true }));
const client = new WebChatClient(config2);
expect(client.options?.ui?.base?.layout).toBe(Layout.MOBILE);
});
test('init aut', async () => {
const auth2 = new AuthClient(config2);
expect(await auth2.initToken()).toBe(true);
expect(auth2.checkOptions()).toBe(true);
const auth3 = new AuthClient(config3);
expect(await auth3.initToken()).toBe(true);
expect(auth3.checkOptions()).toBe(false);
const auth4 = new AuthClient(config4);
expect(await auth4.initToken()).toBe(false);
expect(auth4.checkOptions()).toBe(true);
});
});

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthType } from '@coze-studio/open-chat/types';
import { type CozeChatOptions } from '@/types/client';
export class AuthClient {
readonly options: CozeChatOptions;
public constructor(options: CozeChatOptions) {
this.options = options;
}
public async initToken() {
try {
if (
this.options.auth?.type === AuthType.TOKEN &&
!this.options.auth?.token
) {
const token = await this.options.auth?.onRefreshToken?.('');
this.options.auth.token = token;
if (!token) {
alert(
'The access token is missing. Please check the configuration information.',
);
}
return !!token;
}
} catch (_) {
console.error('[WebSdk Error] initToken error');
alert(
'The access token is missing. Please check the configuration information.',
);
return false;
}
return true;
}
public checkOptions() {
if (this.options.auth?.type !== AuthType.TOKEN) {
console.error("Non-Token is unsupported; auth's type must be token");
alert(
"The auth type (unauth) is unsupported yet; auth's type must be token",
);
return false;
}
if (this.options.auth?.type === AuthType.TOKEN) {
if (!this.options.auth.onRefreshToken) {
console.error('[WebSdk Error] onRefreshToken must be provided');
alert('onRefreshToken must be provided');
return false;
}
if (typeof this.options.auth.onRefreshToken !== 'function') {
console.error('[WebSdk Error] onRefreshToken must be a function');
alert('onRefreshToken must be a function');
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,142 @@
/*
* 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 { isMobileOnly } from 'react-device-detect';
import { createRoot } from 'react-dom/client';
import { nanoid } from 'nanoid';
import { Language, Layout, AuthType } from '@coze-studio/open-chat/types';
import { type CozeChatOptions } from '@/types/client';
import { createGlobalStore, type ClientStore } from '@/store/global';
import CozeClientWidget from '@/components/widget';
import '@coze-common/assets/style/index.less';
import './main.less';
import { AuthClient } from './auth';
const formatOptions = (optionsRaw: CozeChatOptions) => {
const options: CozeChatOptions = optionsRaw;
const layoutDefault = isMobileOnly ? Layout.MOBILE : Layout.PC;
options.config = options.config || {};
options.config.botId =
options.config.botInfo?.botId ||
options.config.botId ||
options.config.bot_id ||
'';
options.ui = optionsRaw.ui || {};
// 小助手 ui基础配置
options.ui.base = Object.assign(
{
layout: optionsRaw.componentProps?.layout || layoutDefault,
lang: optionsRaw.componentProps?.lang || Language.EN,
zIndex: optionsRaw.componentProps?.zIndex,
icon: optionsRaw.componentProps?.icon,
},
optionsRaw.ui?.base || {},
);
// chatBot 配置格式化
options.ui.chatBot = Object.assign(
{
title: optionsRaw.componentProps?.title,
width: optionsRaw.componentProps?.width,
uploadable: optionsRaw.componentProps?.uploadable ?? true,
},
optionsRaw.ui?.chatBot || {},
);
options.ui.asstBtn = Object.assign(
{
isNeed: true,
},
options.ui.asstBtn || {},
);
options.ui.header = Object.assign(
{
isShow: true,
isNeedClose: true,
},
options.ui.header || {},
);
return options;
};
export class WebChatClient {
static clients: WebChatClient[] = [];
private root: ReturnType<typeof createRoot> | undefined;
private readonly defaultRoot?: HTMLDivElement;
private readonly globalStore: ClientStore;
readonly authClient: AuthClient;
readonly chatClientId = nanoid();
readonly options: CozeChatOptions;
readonly senderName: string;
public constructor(options: CozeChatOptions) {
console.info('WebChatClient constructorxxx', options);
this.senderName = `chat-app-sdk-${Date.now()}`;
this.options = formatOptions(options);
this.authClient = new AuthClient(options);
const { el } = this.options;
this.globalStore = createGlobalStore(this);
if (!this.authClient.checkOptions()) {
return;
}
let renderEl: HTMLElement;
if (!el) {
this.defaultRoot = document.createElement('div');
document.body.appendChild(this.defaultRoot);
renderEl = this.defaultRoot;
} else {
renderEl = el;
}
this.root = createRoot(renderEl);
this.root.render(
<CozeClientWidget
client={this}
globalStore={this.globalStore}
position={el ? 'static' : undefined}
/>,
);
WebChatClient.clients.push(this);
}
public showChatBot() {
this.globalStore.getState().setChatVisible(true);
}
public hideChatBot() {
this.globalStore.getState().setChatVisible(false);
}
public async getToken() {
if (this.options.auth?.type === AuthType.TOKEN) {
return await this.options.auth?.onRefreshToken?.('');
}
}
public destroy() {
this.root?.unmount();
if (this.defaultRoot) {
this.defaultRoot.remove();
}
WebChatClient.clients = WebChatClient.clients.filter(c => c !== this);
}
}

View File

@@ -0,0 +1,29 @@
.coze-chat-sdk {
* {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
textarea {
padding: 0;
}
a:focus,
input:focus,
p:focus,
svg:focus,
li:focus,
div:focus,
textarea:focus,
a:focus-visible,
input:focus-visible,
p:focus-visible,
li:focus-visible,
div:focus-visible {
box-shadow: none;
outline: none;
border: none;
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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 cls from 'classnames';
import styles from './index.module.less';
export const Close = ({
classNames,
onClick,
themeType = 'dark',
}: {
classNames?: string;
onClick: () => void;
themeType?: 'dark' | 'light';
}) => (
<div
className={cls(styles.close, classNames, themeType && styles[themeType])}
onClick={onClick}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M4.96977 17.7929C4.57925 18.1834 4.57925 18.8166 4.96977 19.2071C5.3603 19.5976 5.99346 19.5976 6.38399 19.2071L12.1769 13.4142L17.9698 19.2071C18.3603 19.5976 18.9935 19.5976 19.384 19.2071C19.7745 18.8166 19.7745 18.1834 19.384 17.7929L13.5911 12L19.384 6.20711C19.7745 5.81658 19.7745 5.18342 19.384 4.79289C18.9935 4.40237 18.3603 4.40237 17.9698 4.79289L12.1769 10.5858L6.38399 4.79289C5.99347 4.40237 5.3603 4.40237 4.96978 4.79289C4.57925 5.18342 4.57925 5.81658 4.96978 6.20711L10.7627 12L4.96977 17.7929Z"></path>
</svg>
</div>
);

View File

@@ -0,0 +1,38 @@
.close {
display: flex;
align-items: center;
justify-content: center;
height: 24px;
width: 24px;
color: rgba(28,31,35, 80%);
border-radius: 6px;
cursor: pointer;
&:hover {
background: #EEE;
}
&.light {
color: #FFF;
&:hover{
background: none;
}
}
}
@keyframes animation-rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(1turn);
}
}
.spin {
animation: animation-rotate .6s linear infinite;
animation-fill-mode: forwards;
}

View File

@@ -0,0 +1,72 @@
/*
* 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 cls from 'classnames';
import styles from './index.module.less';
export const Spin = ({ classNames }: { classNames?: string }) => (
<div
className={cls(styles.spin, classNames)}
style={{
color: 'rgba(0,100,250, 1)',
}}
>
<svg
width="36"
height="36"
viewBox="0 0 36 36"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
data-icon="spin"
>
<defs>
<linearGradient
x1="0%"
y1="100%"
x2="100%"
y2="100%"
id="linearGradient-17"
>
<stop stop-color="currentColor" stop-opacity="0" offset="0%"></stop>
<stop
stop-color="currentColor"
stop-opacity="0.50"
offset="39.9430698%"
></stop>
<stop stop-color="currentColor" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect
fill-opacity="0.01"
fill="none"
x="0"
y="0"
width="36"
height="36"
></rect>
<path
d="M34,18 C34,9.163444 26.836556,2 18,2 C11.6597233,2 6.18078805,5.68784135 3.59122325,11.0354951"
stroke="url(#linearGradient-17)"
stroke-width="4"
stroke-linecap="round"
></path>
</g>
</svg>
</div>
);

View File

@@ -0,0 +1,58 @@
/*
* 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 cls from 'classnames';
import { Layout } from '@coze-studio/open-chat/types';
import { getCssVars } from '@/util/style';
import { type AstBtnProps } from '@/types/chat';
import { useGlobalStore } from '@/store/context';
import WidgetPng from '@/assets/widget.png';
import styles from './index.module.less';
export const AstBtn: FC<AstBtnProps> = ({ position = 'fixed', client }) => {
const { chatVisible, setChatVisible, layout } = useGlobalStore(s => ({
chatVisible: s.chatVisible,
setChatVisible: s.setChatVisible,
layout: s.layout,
}));
const { base: baseConf, asstBtn: asstBtnConf } = client?.options?.ui || {};
const iconUrl = baseConf?.icon;
const zIndex = baseConf?.zIndex;
const zIndexStyle = getCssVars({ zIndex });
if (chatVisible || !asstBtnConf?.isNeed) {
return null;
}
return (
<div
style={{ position, ...zIndexStyle }}
className={cls(styles['coze-ast-btn'], {
[styles.mobile]: layout === Layout.MOBILE,
})}
onClick={e => {
e.stopPropagation();
setChatVisible(true);
}}
>
<img alt="logo" src={iconUrl || WidgetPng} />
</div>
);
};

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createPortal } from 'react-dom';
import { useState, type FC, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import cls from 'classnames';
import { Layout } from '@coze-studio/open-chat/types';
import { getCssVars } from '@/util/style';
import { type ChatContentProps } from '@/types/chat';
import { useGlobalStore } from '@/store/context';
import { Close } from '../icons/close';
import { ChatNonIframe } from './chat-non-iframe';
import styles from './index.module.less';
const ChatSlot: FC<
ChatContentProps & { isNewCreated: boolean }
// eslint-disable-next-line complexity
> = ({ client, isNewCreated }) => {
const { chatVisible, setChatVisible, layout, themeType } =
useGlobalStore(
useShallow(s => ({
layout: s.layout,
setIframe: s.setIframe,
senderName: s.senderName,
chatVisible: s.chatVisible,
setChatVisible: s.setChatVisible,
themeType: s.themeType,
})),
);
const {
base: baseConf,
chatBot: chatBotConf,
header: headerConf,
} = client?.options?.ui || {};
const zIndex = baseConf?.zIndex;
const zIndexStyle = getCssVars({ zIndex });
const width =
layout === Layout.MOBILE ? undefined : chatBotConf?.width || 460;
if (!chatVisible) {
// 不显示chat框
return null;
}
return (
<div
className={cls(styles.iframeWrapper, 'coze-chat-sdk', {
[styles.mobile]: layout === Layout.MOBILE,
[styles.autoFixContainer]: !isNewCreated,
})}
style={{
display: chatVisible ? 'block' : 'none',
width,
...zIndexStyle,
}}
>
{headerConf?.isNeedClose !== false ? (
<Close
onClick={() => {
setChatVisible(false);
}}
classNames={styles.closeBtn}
themeType={themeType === 'bg-theme' ? 'light' : 'dark'}
/>
) : null}
<ChatNonIframe client={client} />
</div>
);
};
export const ChatContent: FC<ChatContentProps> = ({ client }) => {
const { el } = client?.options?.ui?.chatBot || {};
const [chatContentEl] = useState(() => {
if (el) {
return el;
}
const elCreated = document.createElement('div');
document.body.appendChild(elCreated);
return elCreated;
});
const isNewCreated = chatContentEl !== el;
useEffect(
() => () => {
if (isNewCreated) {
document.body.removeChild(chatContentEl);
}
},
[el, chatContentEl],
);
return createPortal(
<ChatSlot client={client} isNewCreated={isNewCreated} />,
chatContentEl,
);
};

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useEffect, useState, type FC } from 'react';
import { Language } from '@coze-studio/open-chat/types';
import { initI18nInstance, I18n } from '@coze-arch/i18n/raw';
import { I18nProvider } from '@coze-arch/i18n/i18n-provider';
import {
zhCN,
enUS,
ConfigProvider,
LocaleProvider,
} from '@coze-arch/bot-semi';
import { type ChatContentProps } from '@/types/chat';
import { useGlobalStore } from '@/store';
import { NonIframeBot } from './non-iframe-bot';
import { NonIframeApp } from './non-iframe-app';
import styles from './index.module.less';
export const ChatNonIframe: FC<ChatContentProps> = ({ client }) => {
const options = client?.options;
const setImagePreview = useGlobalStore(s => s.setImagePreview);
const setIframeLoaded = useGlobalStore(s => s.setIframeLoaded);
const lang = options?.ui?.base?.lang || Language.EN;
const [i18nReady, setI18nReady] = useState(false);
const locale = lang === Language.ZH_CN ? zhCN : enUS;
const onImageClick = useCallback((extra: { url: string }) => {
setImagePreview(preview => {
preview.url = extra.url;
preview.visible = true;
});
}, []);
useEffect(() => {
setIframeLoaded(true);
}, []);
useEffect(() => {
initI18nInstance({ lng: lang }).then(() => setI18nReady(true));
}, [lang]);
if (!i18nReady) {
return null;
}
return (
<I18nProvider i18n={I18n}>
<ConfigProvider>
<LocaleProvider locale={locale}>
<div className={styles.cozeIframe}>
{options?.config?.type === 'app' ? (
<NonIframeApp client={client} onImageClick={onImageClick} />
) : (
<NonIframeBot client={client} onImageClick={onImageClick} />
)}
</div>
</LocaleProvider>
</ConfigProvider>
</I18nProvider>
);
};

View File

@@ -0,0 +1,85 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import { ImagePreview as ImagePreviewSemi } from '@coze-arch/bot-semi';
import { useGlobalStore } from '@/store';
interface PreviewProps {
zIndex: number;
className?: string;
}
export const ImagePreview: React.FC<PreviewProps> = ({ zIndex, className }) => {
const { imagePreviewUrl, imagePreviewVisible, setImagePreview } =
useGlobalStore(
useShallow(s => ({
imagePreviewVisible: s.imagePreview.visible,
imagePreviewUrl: s.imagePreview.url,
setImagePreview: s.setImagePreview,
})),
);
const onVisibleChange = (visible: boolean) => {
setImagePreview(preview => (preview.visible = visible));
};
const [imageUrl, setImageUrl] = useState(imagePreviewUrl);
useEffect(() => {
setImageUrl(imagePreviewUrl);
(async () => {
if (imagePreviewUrl?.startsWith('blob:')) {
const base64Url = await revertBlobUrlToBase64(imagePreviewUrl);
if (base64Url) {
setImageUrl(base64Url);
}
}
})();
}, [imagePreviewUrl]);
return (
<ImagePreviewSemi
previewCls={className}
zIndex={zIndex}
src={imageUrl}
visible={imagePreviewVisible}
onVisibleChange={onVisibleChange}
/>
);
};
const revertBlobUrlToBase64 = (blobUrl: string): Promise<string | null> =>
new Promise((resolve, reject) => {
(async () => {
try {
const response = await fetch(blobUrl);
const blob = await response.blob();
const reader = new FileReader();
reader.onloadend = () => {
const base64data = reader.result;
resolve(base64data as string);
};
reader.onerror = error => {
console.error('转换过程中出现错误:', error);
resolve(null);
};
reader.readAsDataURL(blob);
} catch (error) {
console.error('转换过程中出现错误:', error);
resolve(null);
}
})();
});

View File

@@ -0,0 +1,111 @@
.coze-ast-btn {
position: fixed;
bottom: 30px;
right: 30px;
display: flex;
justify-content: center;
align-items: center;
height: 56px;
width: 56px;
cursor: pointer;
z-index: var(--coze-z-index-iframe);
transition: transform 0.3s ease;
&:hover {
transform: scale(1.16);
}
&:active {
transform: scale(1.08);
}
> img {
width: 100%;
height: 100%;
}
> svg {
width: 100%;
height: 100%;
}
&.mobile {
bottom: 24px;
right: 24px;
height: 40px;
width: 40px;
&:active {
transform: scale(1.08);
}
}
}
.iframe-wrapper {
position: fixed;
z-index: calc(var(--coze-z-index-iframe) - 1);
background-color: #fff;
&:not(.mobile) {
height: calc(100% - 40px);
min-height: 400px;
max-height: 1200px;
bottom: 20px;
right: 20px;
border-radius: 8px;
box-shadow: 0 6px 8px 0 rgb(29 28 35 / 6%), 0 0 2px 0 rgb(29 28 35 / 18%);
}
.loading {
height: 100%;
width: 100%;
}
.close-btn {
position: absolute;
right: 16px;
top: 12px;
width: 32px;
height: 32px;
z-index: 100;
}
.extra-close {
width: 32px;
height: 32px;
}
.coze-iframe {
width: 100%;
height: 100%;
background: #fff;
border: none;
border-radius: 8px;
overflow: hidden;
}
&.mobile {
inset: 0;
.close-btn {
right: 12px;
top: 24px;
}
.coze-iframe {
border-radius: 0;
}
}
&.auto-fix-container {
position: relative;
width: 100%;
height: 100%;
right: 0;
bottom: 0;
min-height: auto;
max-height: auto;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { getCssVars } from '@/util/style';
import { type CozeWidgetProps, type WidgetAdapterProps } from '@/types/chat';
import { GlobalStoreProvider } from '@/store/context';
import { useMessageInteract } from '@/hooks/use-message-interact';
import { useImagePreview } from '@/hooks/use-image-preview';
import { ImagePreview } from './image-preview';
import { ChatContent } from './chat-content';
import { AstBtn } from './ast-btn';
const IFRAME_INDEX = 2;
const WidgetAdapter: FC<WidgetAdapterProps> = ({ client, position }) => {
useImagePreview(client);
useMessageInteract(client.chatClientId, client.options);
const { base: baseConf } = client?.options?.ui || {};
const zIndex = baseConf?.zIndex;
const zIndexStyle = getCssVars({ zIndex });
return (
<>
<ChatContent client={client} />
<ImagePreview
zIndex={zIndexStyle['--coze-z-index-iframe'] + IFRAME_INDEX}
/>
<AstBtn client={client} position={position} />
</>
);
};
const CozeClientWidget: FC<CozeWidgetProps> = props => (
<GlobalStoreProvider globalStore={props.globalStore}>
<WidgetAdapter {...props} />
</GlobalStoreProvider>
);
export default CozeClientWidget;

View File

@@ -0,0 +1,4 @@
.extra-close {
width: 32px;
height: 32px;
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
import { type FC } from 'react';
import { BuilderChat } from '@coze-studio/open-chat';
import { type ChatContentProps } from '@/types/chat';
import { useGlobalStore } from '@/store';
import styles from './index.module.less';
type IOnImageClick = (extra: { url: string }) => void;
export const NonIframeApp: FC<
ChatContentProps & { onImageClick: IOnImageClick }
> = ({ client, onImageClick }) => {
const options = client?.options;
const setThemeType = useGlobalStore(s => s.setThemeType);
const isNeedExtra = options?.ui?.header?.isNeedClose ?? true;
const areaUi = {
showInputArea: true,
isDisabled: false,
uploadable: options?.ui?.chatBot?.uploadable,
isNeedClearContext: options?.ui?.chatBot?.isNeedClearContext ?? true,
isNeedClearMessage: false,
isNeedAddNewConversation:
options?.ui?.chatBot?.isNeedAddNewConversation ?? true,
isNeedFunctionCallMessage:
options?.ui?.chatBot?.isNeedFunctionCallMessage ?? true,
isNeedQuote: options?.ui?.chatBot?.isNeedQuote,
feedback: options?.ui?.chatBot?.feedback,
header: {
isShow: true,
title: options?.ui?.chatBot?.title,
icon: options?.ui?.base?.icon,
...options?.ui?.header,
extra: isNeedExtra ? <div className={styles['extra-close']} /> : null,
},
conversations: options?.ui?.conversations,
input: {
isNeedAudio: options?.ui?.chatBot?.isNeedAudio,
},
footer: options?.ui?.footer,
};
return (
<BuilderChat
workflow={{
id: options?.config?.appInfo?.workflowId,
parameters: {
...options?.config?.appInfo?.parameters,
},
}}
project={{
type: 'app',
mode: 'websdk',
id: options?.config?.appInfo?.appId || '',
conversationName: 'Default', // 走兜底逻辑
layout: options?.ui?.base?.layout,
version: options?.config.appInfo?.version,
}}
userInfo={{
url: options?.userInfo?.url || '',
nickname: options?.userInfo?.nickname || '',
id: options?.userInfo?.id || '',
}}
areaUi={areaUi}
auth={{
type: 'external',
token: options?.auth?.token,
refreshToken: options?.auth?.onRefreshToken,
}}
eventCallbacks={{
onImageClick,
onThemeChange: setThemeType,
}}
/>
);
};

View File

@@ -0,0 +1,8 @@
.chat-app-wrapper {
height: 100%;
}
.extra-close {
width: 32px;
height: 32px;
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC } from 'react';
import { WebSdkChat } from '@coze-studio/open-chat';
import { getChatConfig } from '@/util/get-chat-config';
import { type ChatContentProps } from '@/types/chat';
import { useGlobalStore } from '@/store';
import styles from './index.module.less';
type IOnImageClick = (extra: { url: string }) => void;
export const NonIframeBot: FC<
ChatContentProps & { onImageClick: IOnImageClick }
> = props => {
const title = props.client.options.ui?.chatBot?.title;
const icon = props.client.options.ui?.base?.icon;
const headerExtra = props.client.options.ui?.header?.isNeedClose ? (
<div className={styles['extra-close']} />
) : null;
const layout = props.client.options.ui?.base?.layout;
const { onImageClick } = props;
const { userInfo } = props.client.options;
const setThemeType = useGlobalStore(s => s.setThemeType);
const iframeParams = getChatConfig(
props.client.chatClientId,
props.client.options,
);
if (iframeParams.chatConfig.auth) {
iframeParams.chatConfig.auth.onRefreshToken =
props.client.options.auth?.onRefreshToken;
}
return (
<div className={styles.chatAppWrapper}>
<WebSdkChat
title={title || ''}
icon={icon}
chatConfig={iframeParams.chatConfig}
headerExtra={headerExtra}
layout={layout}
style={{
height: '100%',
}}
onImageClick={onImageClick}
onThemeChange={setThemeType}
userInfo={userInfo}
/>
</div>
);
};

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RouterProvider } from 'react-router-dom';
import { type FC, useEffect, useState } from 'react';
import { initI18nInstance, I18n } from '@coze-arch/i18n/raw';
import { I18nProvider } from '@coze-arch/i18n/i18n-provider';
import { devRouter } from './routes';
const DevApp: FC = () => {
const [i18nReady, setI18nReady] = useState(false);
useEffect(() => {
initI18nInstance().then(() => setI18nReady(true));
}, []);
if (!i18nReady) {
return null;
}
return (
<I18nProvider i18n={I18n}>
<RouterProvider router={devRouter} />
</I18nProvider>
);
};
export default DevApp;

View File

@@ -0,0 +1,28 @@
/*
* 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 './main.less';
import { createRoot } from 'react-dom/client';
import DevApp from './App';
const rootEl = document.createElement('div');
rootEl.setAttribute('className', 'coze-chat-sdk');
document.body.append(rootEl);
const root = createRoot(rootEl);
root.render(<DevApp />);

View File

@@ -0,0 +1,11 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body,
#root {
height: 100%;
padding: 0;
margin: 0;
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, useState } from 'react';
import { nanoid } from 'nanoid';
import { OpenApiSource } from '@coze-studio/open-chat/types';
import { WebSdkChat } from '@coze-studio/open-chat';
const uid = nanoid();
const botConfig = {
bot_id: process.env.CHAT_APP_INDEX_COZE_BOT_ID || '',
user: uid,
conversation_id: uid,
source: OpenApiSource.WebSdk,
};
const TestAppWidget: FC = () => {
const [visible] = useState(false);
// 触发更新
return (
<>
{visible ? (
<WebSdkChat
title="客服小助手"
chatConfig={botConfig}
style={{ height: 800 }}
useInIframe={false}
/>
) : null}
</>
);
};
export default TestAppWidget;

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { nanoid } from 'nanoid';
import { OpenApiSource } from '@coze-studio/open-chat/types';
import { WebSdkChat } from '@coze-studio/open-chat';
const uid = nanoid();
const botConfig = {
user: uid,
conversation_id: uid,
bot_id: process.env.CHAT_APP_INDEX_COZE_BOT_ID || '',
source: OpenApiSource.WebSdk,
};
const TestChatDemo: FC = () => (
<WebSdkChat
title="客服小助手"
chatConfig={botConfig}
className="absolute top-[50px]"
useInIframe={false}
style={{
position: 'absolute',
left: 50,
top: 50,
width: 460,
height: 700,
}}
/>
);
export default TestChatDemo;

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