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

@@ -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;

View File

@@ -0,0 +1,119 @@
/*
* 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 } from 'react';
import {
AuthType,
ChatType,
Language,
Layout,
} from '@coze-studio/open-chat/types';
import { WebChatClient } from '@/client';
export const TestClientDemo = () => {
useEffect(() => {
new WebChatClient({
config: {
type: ChatType.BOT,
appInfo: {
appId: process.env.CHAT_APP_CHATFLOW_COZE_APP_ID || '',
workflowId: process.env.CHAT_APP_CHATFLOW_COZE_WORKFLOW_ID || '',
},
botInfo: {
botId: process.env.CHAT_APP_INDEX_COZE_BOT_ID || '',
parameters: {
botId: process.env.CHAT_APP_INDEX_COZE_BOT_ID || '',
botName: '历史学教授',
},
},
},
auth: {
type: AuthType.TOKEN,
token: process.env.CHAT_APP_COZE_TOKEN || '',
onRefreshToken: () => process.env.CHAT_APP_COZE_TOKEN || '',
},
componentProps: {
title: '历史学教授',
lang: Language.ZH_CN,
layout: Layout.PC,
},
extra: {
webChat: {
test: '123123',
},
},
ui: {
asstBtn: {
isNeed: true,
},
chatBot: {
title: '历史学教授33',
uploadable: true,
isNeedAudio: true,
isNeedFunctionCallMessage: true,
isNeedQuote: true,
feedback: {
isNeedFeedback: true,
feedbackPanel: {
title:
'起来不是一个有明确意义的旅游相关问题哦。你可以告诉我关于旅游的具体问题,比如想去的旅游目的地、旅游预算、旅游方',
tags: [
{
label: '信息不正确',
},
{
label: '涉及敏感信息',
isNeedDetail: true,
},
],
},
},
},
header: {
isShow: true,
isNeedClose: true,
},
footer: {
isShow: true,
expressionText: ' 由{{name}}提sdd供',
linkvars: {
name: {
text: 'Coze',
link: 'https://www.coze.com',
},
},
},
conversations: {
isNeed: true,
},
base: {
layout: Layout.PC,
lang: Language.ZH_CN,
},
},
userInfo: {
id: '12334',
url: process.env.CHAT_APP_COZE_BOT_USER_URL || '',
nickname: '3qweqweq4we',
},
});
}, []);
return <div></div>;
};

View File

@@ -0,0 +1,45 @@
/*
* 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 { createBrowserRouter, Outlet } from 'react-router-dom';
import TestAppWidget from '@/dev-app/page/AppWidget';
import { TestClientDemo } from '../page/Client';
import TestChatDemo from '../page/Chat';
const Layout = () => <Outlet />;
export const devRouter: ReturnType<typeof createBrowserRouter> =
createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
path: 'chat',
element: <TestChatDemo />,
},
{
path: 'app_widget',
element: <TestAppWidget />,
},
{
path: 'client',
element: <TestClientDemo />,
},
],
},
]);

View File

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

View File

@@ -0,0 +1,64 @@
/*
* 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 } from 'react';
import {
type PostMessage,
PostMessageEvent,
} from '@coze-studio/open-chat/types';
import { postMessageUtil } from '@/types/post';
import { useGlobalStore } from '@/store';
import { type WebChatClient } from '@/client';
export const useImagePreview = (client: WebChatClient) => {
const { setImagePreview } = useGlobalStore(s => ({
setImagePreview: s.setImagePreview,
}));
const onMessageHandler = useCallback<{
(event: MessageEvent<PostMessage>): void;
}>(
event => {
const msg = event?.data;
if (msg.chatStoreId !== client.chatClientId) {
return;
}
switch (msg.event) {
case PostMessageEvent.ImageClick:
// @ts-expect-error -- linter-disable-autofix
if (postMessageUtil.isImageClick(msg)) {
setImagePreview(preview => {
preview.url = msg.payload.url;
preview.visible = true;
});
}
break;
default:
}
},
[setImagePreview, client],
);
useEffect(() => {
window.addEventListener('message', onMessageHandler);
return () => {
window.removeEventListener('message', onMessageHandler);
};
}, [onMessageHandler]);
};

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import {
type IframeParams,
IframeMessageEvent,
WebSdkError,
} from '@coze-studio/open-chat/types';
import { PostMessageChannel } from '@coze-arch/bot-utils/post-message-channel';
import { getChatConfig } from '@/util/get-chat-config';
import { type CozeChatOptions } from '@/types/client';
import { useGlobalStore } from '@/store/context';
export const useMessageInteract = (
chatClientId: string,
cozeChatOption: CozeChatOptions,
) => {
const refPostChannel = useRef<PostMessageChannel>();
//const [themeType, setThemeType] = useState<'bg-theme' | 'light'>('light');
const {
iframe: iframeEl,
senderName,
setThemeType,
} = useGlobalStore(
useShallow(s => ({
iframe: s.iframe,
senderName: s.senderName,
setThemeType: s.setThemeType,
})),
);
const refProps = useRef<{
chatClientId: string;
cozeChatOption: CozeChatOptions;
}>({
chatClientId,
cozeChatOption,
});
refProps.current = {
chatClientId,
cozeChatOption,
};
useEffect(() => {
if (iframeEl?.contentWindow) {
refPostChannel.current = new PostMessageChannel({
channelPort: iframeEl.contentWindow,
senderName,
});
refPostChannel.current.onRequest<string | undefined, IframeParams>(
IframeMessageEvent.GET_IFRAME_PARAMS,
() => {
const iframeParams = getChatConfig(
refProps.current.chatClientId,
refProps.current.cozeChatOption,
);
return {
code: 0,
data: iframeParams,
};
},
);
refPostChannel.current.onRequest<string | undefined, string | undefined>(
IframeMessageEvent.GET_NEW_TOKEN,
async token => {
let tokenNew;
try {
tokenNew =
await refProps.current.cozeChatOption.auth?.onRefreshToken?.(
token || '',
);
} catch (e) {
console.error('[WebSdk Error] Get Token Error');
}
if (tokenNew) {
return {
code: 0,
data: tokenNew,
};
} else {
return {
code: WebSdkError.AUTH_TOKEN_GET_FAILED,
message: 'Get Token Error',
};
}
},
);
refPostChannel.current.onRequest<string | undefined, string | undefined>(
IframeMessageEvent.THEME_CHANGE,
theme => {
setThemeType(theme as 'bg-theme' | 'light');
return {
code: 0,
};
},
);
return () => {
refPostChannel.current?.destory();
refPostChannel.current = undefined;
};
}
}, [iframeEl, senderName]);
};

View File

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

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, test, expect, vi } from 'vitest';
import { AuthType } from '@coze-studio/open-chat/types';
import { createGlobalStore } from '@/store/global';
import { WebChatClient } from '@/client';
import type * as ClientModule from '@/client';
vi.hoisted(() => {
// @ts-expect-error -- 将 IS_OVERSEA 提升到最外层
global.IS_OVERSEA = false;
});
vi.mock('./auth', () => ({
AuthClient: vi.fn().mockImplementation(() => ({
initToken: vi.fn().mockResolvedValue(false),
checkOptions: vi.fn().mockReturnValue(true),
})),
}));
vi.mock('@/components/widget', () => ({
default: vi.fn(),
}));
vi.mock('@coze-studio/open-chat', () => ({
postErrorMessage: vi.fn(),
ChatSdkErrorType: {
OPEN_API_ERROR: 'OPEN_API_ERROR',
},
}));
describe('createGlobalStore setter', () => {
console.log('[dev]...', WebChatClient);
test('simple states', async () => {
const store = createGlobalStore(
new WebChatClient({
config: {
botId: '',
},
}),
);
const { setIframe, setChatVisible, setIframeLoaded, setImagePreview } =
store.getState();
const iframe = document.createElement('iframe');
setIframe(iframe);
await setChatVisible(true);
setIframeLoaded(false);
setImagePreview(preview => {
preview.url = 'xxx';
preview.visible = true;
});
store.getState().setThemeType('bg-theme');
expect(store.getState().themeType).toBe('bg-theme');
expect(store.getState()).toEqual(
expect.objectContaining({
iframe,
chatVisible: true,
iframeLoaded: false,
}),
);
expect(store.getState().imagePreview).toMatchObject({
url: 'xxx',
visible: true,
});
});
test('token invalid', async () => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { WebChatClient: Client } =
await vi.importActual<typeof ClientModule>('@/client');
const store = createGlobalStore(
new Client({
config: {
botId: '',
},
auth: {
type: AuthType.TOKEN,
token: '',
onRefreshToken: () => '',
},
}),
);
const { setChatVisible } = store.getState();
await setChatVisible(true);
expect(store.getState().chatVisible).toBe(false);
});
});

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode, createContext, type FC, useContext } from 'react';
import { useStoreWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/shallow';
import { type ClientStateAction, type ClientStore } from './global';
export const GlobalStoreContext = createContext<{
globalStore: ClientStore;
// @ts-expect-error -- linter-disable-autofix
}>(undefined);
export const GlobalStoreProvider: FC<{
children: ReactNode;
globalStore: ClientStore;
}> = ({ children, globalStore }) => (
<GlobalStoreContext.Provider
value={{
globalStore,
}}
>
{children}
</GlobalStoreContext.Provider>
);
export const useGlobalStore: <T>(
selector: (store: ClientStateAction) => T,
) => T = selector => {
const store = useContext(GlobalStoreContext).globalStore;
return useStoreWithEqualityFn(store, selector, shallow);
};

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { devtools, subscribeWithSelector } from 'zustand/middleware';
import { create } from 'zustand';
import { produce } from 'immer';
import { type ImagePreview, Layout } from '@coze-studio/open-chat/types';
import { ChatSdkErrorType, postErrorMessage } from '@coze-studio/open-chat';
import { type AuthClient } from '@/client/auth';
import { type WebChatClient } from '@/client';
interface ClientState {
chatVisible: boolean;
iframe?: HTMLIFrameElement;
iframeLoaded: boolean;
imagePreview: ImagePreview;
layout: Layout;
senderName: string;
themeType: 'bg-theme' | 'light';
}
interface ClientAction {
setIframe: (el?: HTMLIFrameElement) => void;
setIframeLoaded: (loaded: boolean) => void;
setChatVisible: (visible: boolean) => void;
setImagePreview: (recipe: (preview: ImagePreview) => void) => void;
setThemeType: (themeType: 'bg-theme' | 'light') => void;
}
export type ClientStateAction = ClientState & ClientAction;
export const createGlobalStore = (client: WebChatClient) => {
const { options, senderName } = client;
const { layout = Layout.PC } = options?.ui?.base ?? {};
const authClient = client.authClient as AuthClient;
const defaultState: ClientState = {
chatVisible: false,
iframeLoaded: false,
themeType: 'light',
imagePreview: {
url: '',
visible: false,
},
layout,
senderName,
};
return create<ClientStateAction>()(
devtools(
subscribeWithSelector(set => ({
...defaultState,
setThemeType: (themeType: 'bg-theme' | 'light') => {
set({
themeType,
});
},
setIframe: element => {
set({
iframe: element,
});
},
setChatVisible: async visible => {
const chatBot = options?.ui?.chatBot;
if (authClient && !(await authClient.initToken())) {
postErrorMessage({
type: ChatSdkErrorType.OPEN_API_ERROR,
code: 401,
message: 'invalid token',
});
return;
}
// 判断是否能够显示、隐藏
if (visible) {
if ((await chatBot?.onBeforeShow?.()) === false) {
return;
}
} else {
if ((await chatBot?.onBeforeHide?.()) === false) {
return;
}
}
set({
chatVisible: visible,
});
// 显示、隐藏后的回调。
if (visible) {
chatBot?.onShow?.();
} else {
chatBot?.onHide?.();
}
},
setIframeLoaded: loaded => {
set({
iframeLoaded: loaded,
});
},
setImagePreview: recipe =>
set(
produce<ClientState>(draft => {
recipe(draft.imagePreview);
}),
),
})),
{
enabled: IS_DEV_MODE,
name: 'sdkChatApp.global',
},
),
);
};
export type ClientStore = ReturnType<typeof createGlobalStore>;

View File

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

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.
*/
vi.mock('@/client', () => ({
default: vi.fn(),
WebChatClient: vi.fn(),
}));
vi.mock('@coze-studio/open-chat', () => ({
default: vi.fn(),
}));
vi.stubGlobal('IS_DEV_MODE', false);
beforeEach(() => {
global.alert = info => {
console.log(info);
};
});

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ClientStore } from '@/store/global';
import { type WebChatClient } from '@/client';
export interface CozeWidgetProps {
position?: 'static' | 'fixed';
client: WebChatClient;
globalStore: ClientStore;
}
export type WidgetAdapterProps = Pick<CozeWidgetProps, 'position' | 'client'>;
export type AstBtnProps = WidgetAdapterProps;
export type ChatIframProps = Pick<CozeWidgetProps, 'client'>;
export type ChatContentProps = Pick<CozeWidgetProps, 'client'>;

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type ComponentProps,
type AuthProps,
type UiProps,
type OpenUserInfo,
type ChatType,
type AppInfo,
type BotInfo,
} from '@coze-studio/open-chat/types';
export interface CozeChatOptions {
config: {
/** @deprecated 该使用方式已废弃请使用botId */
bot_id?: string;
/** @deprecated 该使用方式已废弃请使用botId */
botId?: string;
/** @deprecated 该字段已废弃请使用auth字段进行配置 */
sdk_verify_token?: string;
type?: ChatType;
appInfo?: AppInfo;
botInfo?: BotInfo;
};
extra?: {
webChat: Record<string, string>;
};
auth?: AuthProps;
userInfo?: OpenUserInfo;
ui?: UiProps;
/** @deprecated 该使用方式已废弃请使用ui属性对ui进行配置 */
el?: HTMLElement;
/** @deprecated 该使用方式已废弃请使用ui属性对ui进行配置 */
componentProps?: ComponentProps; // 待废弃
}

View File

@@ -0,0 +1,34 @@
/*
* 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 PostMessage,
PostMessageEvent,
} from '@coze-studio/open-chat/types';
export const postMessageUtil = {
isImageClick(
msg: PostMessage<Partial<PostMessageData['ImageClick']>>,
): msg is PostMessage<PostMessageData['ImageClick']> {
return msg.event === PostMessageEvent.ImageClick && !!msg?.payload?.url;
},
};
export interface PostMessageData {
[PostMessageEvent.ImageClick]: {
url: string;
};
}

View File

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

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 { nanoid } from 'nanoid';
import { pick } from 'lodash-es';
import { ChatType } from '@coze-studio/open-chat/types';
import { OpenApiSource, type IframeParams } from '@coze-studio/open-chat/types';
import { type CozeChatOptions } from '@/types/client';
export const getChatConfig = (
chatClientId: string,
cozeChatOption: CozeChatOptions,
): IframeParams => {
const { config, auth, userInfo, ui, extra } = cozeChatOption;
return {
chatClientId,
chatConfig: {
type: config?.type || ChatType.BOT,
bot_id: (config?.botId ?? config?.bot_id) || '',
appInfo: config?.appInfo,
botInfo: config?.botInfo,
conversation_id: nanoid(),
extra,
ui: {
base: pick(ui?.base || {}, ['icon', 'lang', 'layout']),
chatBot: pick(ui?.chatBot || {}, [
'title',
'uploadable',
'isNeedClearContext',
'isNeedClearMessage',
'isNeedAudio',
'isNeedFunctionCallMessage',
'isNeedQuote',
'isNeedAddNewConversation',
'feedback',
]),
footer: ui?.footer,
header: ui?.header,
conversations: ui?.conversations,
},
auth: pick(auth || {}, ['type', 'token']),
source: OpenApiSource.WebSdk,
},
userInfo,
};
};

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.
*/
const clientCssVars = {
'--coze-z-index-iframe': 1000,
};
export const getCssVars = ({ zIndex }: { zIndex?: number }) => ({
...clientCssVars,
...(typeof zIndex === 'number'
? {
'--coze-z-index-iframe': zIndex,
}
: {}),
});

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.
*/
/** @type {import('tailwindcss').Config} */
const { genTailwindPlugin } = require('@coze-arch/tailwind-config/util');
module.exports = {
darkMode: 'class',
presets: [require('@coze-arch/tailwind-config')],
important: '.coze-chat-sdk',
content: [
'./src/**/*.{html,tsx}',
'../../../../../packages/components/coze-design/src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@coze-arch/coze-design/dist/**/*.{js,jsx,css}',
'../open-chat/src/**/*.{js,ts,jsx,tsx}',
'../../../common/chat-area/chat-area/src/**/*.{js,ts,jsx,tsx}',
'../../../common/chat-area/chat-uikit/src/**/*.{js,ts,jsx,tsx}',
'../../../common/chat-area/plugin-chat-shortcuts/src/**/*.{js,ts,jsx,tsx}',
],
corePlugins: {
preflight: false, // 关闭@tailwind base默认样式避免对现有样式影响https://code.byted.org/obric/bot-studio-monorepo/merge_requests/2945
},
plugins: [genTailwindPlugin(':root', '.dark .coze-chat-sdk-dark')],
};

View File

@@ -0,0 +1,62 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"preserveSymlinks": false,
"skipLibCheck": true,
"outDir": "./dist_ignore",
"types": [],
"tsBuildInfoFile": "dist_ignore/tsconfig.build.tsbuildinfo",
"sourceMap": false
},
"include": ["src", "src/**/*.json"],
"exclude": ["./src/**/*.test.ts", "src/**/__tests__/**", "src/test/setup.ts"],
"references": [
{
"path": "../../../arch/bot-env-adapter/tsconfig.build.json"
},
{
"path": "../../../arch/bot-env/tsconfig.build.json"
},
{
"path": "../../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../../arch/bot-utils/tsconfig.build.json"
},
{
"path": "../../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../../common/assets/tsconfig.build.json"
},
{
"path": "../../../components/bot-semi/tsconfig.build.json"
},
{
"path": "../../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../../config/postcss-config/tsconfig.build.json"
},
{
"path": "../../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../../config/tailwind-config/tsconfig.build.json"
},
{
"path": "../../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../../../../infra/plugins/pkg-root-webpack-plugin/tsconfig.build.json"
},
{
"path": "../open-chat/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"rootDir": "./",
"types": ["vitest/globals", "node"],
"paths": {
"@/*": ["./src/*"]
},
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": [
"src/**/*.json",
"src",
"rspack-config",
"stories",
"vitest.config.ts"
],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
test: {
setupFiles: ['src/test/setup.ts'],
},
});

View File

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

View File

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

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, useRef } from 'react';
import styles from './index.module.less';
export const Background: FC<{
bgInfo?: {
imgUrl: string;
themeColor: string; // 背景颜色
};
}> = props => {
const targetRef = useRef(null);
const { bgInfo } = props;
if (!bgInfo || !bgInfo?.imgUrl) {
return null;
}
const { themeColor = 'transparent', imgUrl } = bgInfo;
return (
<div
ref={targetRef}
className={styles['bg-image']}
style={{
backgroundColor: themeColor,
}}
>
<div className={styles.mask} />
<div className={styles['img-container']}>
{imgUrl ? <img src={imgUrl} className={styles.img} /> : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createContext,
type FC,
type PropsWithChildren,
useContext,
} from 'react';
import { useUpdateEffect } from 'ahooks';
import { type IBuilderChatProps } from '../type';
import { combineAppDataWithProps } from '../services/get-bot-info';
import { type InitData } from '../data-type';
interface BuilderChatContextValue {
appDataFromOnLine?: InitData | null;
appDataCombineWithProps?: InitData | null;
}
type BuilderChatContextProps = BuilderChatContextValue & {
setAppDataFromOnLine?: (appDataFromOnLint: InitData | null) => void;
setAppDataCombineWithProps?: (
appDataCombineWithProps: InitData | null,
) => void;
};
const BuilderChatContext = createContext<BuilderChatContextProps>({
appDataFromOnLine: null,
appDataCombineWithProps: null,
});
export const BuilderChatProvider: FC<
PropsWithChildren<BuilderChatContextProps>
> = ({ children, ...props }) => (
<BuilderChatContext.Provider value={props} children={children} />
);
export const useGetAppDataFromOnLine = () => {
const { appDataFromOnLine } = useContext(BuilderChatContext);
return appDataFromOnLine;
};
export const useGetAppDataCombineWithProps = () => {
const { appDataCombineWithProps } = useContext(BuilderChatContext);
return appDataCombineWithProps;
};
export const useSetAppDataFromOnLine = () => {
const { setAppDataFromOnLine } = useContext(BuilderChatContext);
return setAppDataFromOnLine;
};
export const useUpdateAppDataCombineWithProps = (props: IBuilderChatProps) => {
const { appDataFromOnLine, setAppDataCombineWithProps } =
useContext(BuilderChatContext);
useUpdateEffect(() => {
if (appDataFromOnLine) {
const formatAPPInfo = combineAppDataWithProps(appDataFromOnLine, props);
setAppDataCombineWithProps?.(formatAPPInfo);
}
}, [appDataFromOnLine, props]);
};

View File

@@ -0,0 +1,341 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useEffect,
useImperativeHandle,
useMemo,
type FC,
forwardRef,
type Ref,
useRef,
type PropsWithChildren,
useState,
memo,
} from 'react';
import cls from 'classnames';
import { type InputController } from '@coze-common/chat-uikit-shared';
import { ContentType } from '@coze-common/chat-core/message/types';
import {
useSendTextMessage,
useSendNormalizedMessage,
} from '@coze-common/chat-area/hooks/messages/use-send-message';
import { useClearContext } from '@coze-common/chat-area/hooks/messages/use-clear-context';
import { useInitStatus } from '@coze-common/chat-area/hooks/context/use-init-status';
import { I18n } from '@coze-arch/i18n';
import type { StudioChatProviderProps } from '@/types/props';
import { Layout } from '@/types/client';
import { useGetTheme } from '@/components/studio-open-chat/hooks/use-get-theme';
import {
OpenChatProvider,
StudioChatArea,
} from '@/components/studio-open-chat';
import { Loading } from '@/components/loading';
import { ChatHeader } from '@/components/header';
import ChatFooter from '@/components/footer';
import ErrorFallback, {
type InitErrorFallback,
} from '@/components/error-fallback';
import { ErrorBoundary } from '@/components/error-boundary';
import {
type IBuilderChatProps,
type MessageType,
type BuilderChatRef,
} from './type';
import { getBuilderEventCallbackPlugin } from './plugins/event-callback';
import { useRequestInit } from './hooks/use-request-init';
import { useOnboardingUpdate } from './hooks/use-on-boarding-update';
import { useInitChat } from './hooks/use-init-chat';
import { useCoreManager } from './hooks/use-core-manager';
import { useBotAndUserUpdate } from './hooks/use-bot-user-update';
import { type InitData } from './data-type';
import {
BuilderChatProvider,
useGetAppDataCombineWithProps,
useUpdateAppDataCombineWithProps,
} from './context/builder-chat-context';
import { Background } from './components/background';
import { AuditPanel } from './components/audit-panel';
import styles from './index.module.less';
export { type BuilderChatRef };
export const BuilderChatContent = forwardRef(
(
{
uiBuilderProps,
chatProps,
}: {
chatProps: StudioChatProviderProps;
uiBuilderProps: IBuilderChatProps;
},
ref: Ref<BuilderChatRef>,
) => {
const refHasInitController = useRef(false);
const refInputController = useRef<InputController>();
const { areaUi } = uiBuilderProps;
const handleClearContext = useClearContext();
const sendMessage = useSendNormalizedMessage();
const sendTextMessage = useSendTextMessage();
useOnboardingUpdate();
useBotAndUserUpdate();
useImperativeHandle(
ref,
() => ({
sendMessage: (message: MessageType) => {
if (message.type === ContentType.Text) {
sendTextMessage({ text: message.text, mentionList: [] }, 'other');
} else if (message.type === ContentType.Image) {
sendMessage(
{
payload: {
contentType: ContentType.Image,
contentObj: {
image_list: [message.value],
},
mention_list: [],
},
},
'other',
);
} else if (message.type === 'file') {
sendMessage(
{
payload: {
contentType: ContentType.File,
contentObj: {
file_list: [message.value],
},
mention_list: [],
},
},
'other',
);
}
},
clearContext: () => {
handleClearContext?.();
},
}),
[handleClearContext, sendTextMessage, sendMessage],
);
useEffect(() => {
refInputController.current?.setInputText?.(
areaUi?.input?.defaultText || '',
);
}, [areaUi?.input?.defaultText]);
const renderChatInputTopSlot = areaUi?.input?.renderChatInputTopSlot
? () => areaUi?.input?.renderChatInputTopSlot?.(false)
: undefined;
const isMobile = uiBuilderProps.project?.layout === Layout.MOBILE;
const theme = useGetTheme();
const { header } = uiBuilderProps.areaUi || {};
return (
<StudioChatArea
{...chatProps}
{...(areaUi || {})}
coreAreaClassName={styles['core-area']}
inputPlaceholder={
areaUi?.input?.placeholder || I18n.t('chatInputPlaceholder')
}
messageMaxWidth={
uiBuilderProps?.project?.mode !== 'websdk' ? '600px' : undefined
}
enableMultimodalUpload={true}
showInputArea={areaUi?.input?.isShow}
messageGroupListClassName={styles['scroll-view']}
renderChatInputTopSlot={renderChatInputTopSlot}
isShowClearContextDivider={true}
headerNode={
<ChatHeader
title={header?.title || ''}
iconUrl={header?.icon}
extra={header?.extra}
theme={theme}
isMobile={isMobile}
isShowConversations={false} // app 这期不支持
isShowHeader={header?.isShow}
/>
}
isMiniScreen={areaUi?.uiTheme === 'chatFlow' ? true : false}
inputNativeCallbacks={{
getController: inputControllerIn => {
refInputController.current = inputControllerIn;
if (!refHasInitController.current) {
refInputController.current?.setInputText?.(
areaUi?.input?.defaultText || '',
);
refHasInitController.current = true;
}
},
}}
/>
);
},
);
const getErrorCallbackComp =
(props: IBuilderChatProps & { refresh: () => void }): FC<InitErrorFallback> =>
({ error, onBeforeRetry }) => (
<>
<ErrorFallback
error={error}
onBeforeRetry={onBeforeRetry}
refresh={props.refresh}
/>
{props.areaUi?.input?.renderChatInputTopSlot?.(true)}
</>
);
const BuilderChatWrap: FC<PropsWithChildren<IBuilderChatProps>> = ({
children,
...props
}) => {
const initStatus = useInitStatus();
const theme = useGetTheme();
const { footer } = props.areaUi || {};
const isMobile = props.project?.layout === Layout.MOBILE;
const appInfoResult = useGetAppDataCombineWithProps();
const footerConfig = {
...(footer || {
expressionText: '',
}),
};
if (props.project?.mode !== 'websdk') {
if (!footerConfig.expressionText) {
footerConfig.expressionText = I18n.t('chat_GenAI_tips');
}
}
if (initStatus !== 'initSuccess') {
return props?.areaUi?.renderLoading?.() || <Loading />;
}
return (
<div
className={cls(styles.content, {
[styles.mobile]: isMobile,
[styles['bg-theme']]: theme === 'bg-theme',
})}
style={props.style}
>
<div
className={cls(styles.area, {
[styles['chat-flow-area']]: props.areaUi?.uiTheme === 'chatFlow',
[styles['chat-ui-builder']]: props.areaUi?.uiTheme === 'uiBuilder',
})}
>
<Background bgInfo={appInfoResult?.customBgInfo} />
{children}
</div>
<ChatFooter {...footerConfig} theme={theme} />
</div>
);
};
const BuilderChatContainer = memo(
forwardRef((props: IBuilderChatProps, ref: Ref<BuilderChatRef>) => {
const { chatProps, hasReady, error, refresh } = useInitChat(props);
const openRequestInit = useRequestInit(props);
const builderEventCallbackPlugin = getBuilderEventCallbackPlugin({
eventCallbacks: props.eventCallbacks,
});
const appInfoResult = useGetAppDataCombineWithProps();
useUpdateAppDataCombineWithProps(props);
const plugins = [builderEventCallbackPlugin];
const requestManagerOptions = useCoreManager(props);
const userInfo = useMemo(
() => ({
url: '',
nickname: '',
...(props.userInfo || {}),
id: props?.userInfo?.id || chatProps?.userInfo?.id || '',
}),
[props?.userInfo, chatProps],
);
const ErrorFallbackComp = getErrorCallbackComp({ ...props, refresh });
if (props?.project?.mode === 'audit') {
return <AuditPanel {...props} />;
}
if (error) {
return <ErrorFallbackComp error={null} refresh={refresh} />;
}
if (!chatProps || !hasReady) {
return props?.areaUi?.renderLoading?.() || <Loading />;
}
const isCustomBackground = !!appInfoResult?.customBgInfo?.imgUrl || false;
console.log(
'[result] isCustomBackground:',
isCustomBackground,
appInfoResult?.customBgInfo,
);
return (
<OpenChatProvider
{...chatProps}
userInfo={userInfo}
openRequestInit={openRequestInit}
plugins={plugins}
requestManagerOptions={requestManagerOptions}
initErrorFallbackFC={ErrorFallbackComp}
onImageClick={props.eventCallbacks?.onImageClick}
debug={props.debug}
isCustomBackground={isCustomBackground}
onThemeChange={props?.eventCallbacks?.onThemeChange}
readonly={props?.areaUi?.isDisabled}
spaceId={props?.spaceId}
>
<BuilderChatWrap {...props}>
<BuilderChatContent
ref={ref}
uiBuilderProps={props}
chatProps={chatProps}
/>
</BuilderChatWrap>
</OpenChatProvider>
);
}),
);
export const BuilderChatWeb = forwardRef(
(props: IBuilderChatProps, ref: Ref<BuilderChatRef>) => {
const [appDataFromOnLine, setAppDataFromOnLine] = useState<InitData | null>(
null,
);
const [appDataCombineWithProps, setAppDataCombineWithProps] =
useState<InitData | null>(null);
return (
<ErrorBoundary>
<BuilderChatProvider
{...{
appDataFromOnLine,
setAppDataFromOnLine,
appDataCombineWithProps,
setAppDataCombineWithProps,
}}
>
<BuilderChatContainer ref={ref} {...props} />
</BuilderChatProvider>
</ErrorBoundary>
);
},
);

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type MixInitResponse } from '@coze-common/chat-area';
import { type EInputMode } from '@/types/props';
export interface ProjectInfoResp {
data: {
icon_url: string;
name: string;
};
}
export interface SuggestPromoteInfo {
suggestReplyMode?: number;
customizedSuggestPrompt?: string;
}
export interface BackgroundImageResp {
theme_color?: string;
gradient_position: {
left?: number;
right?: number;
};
canvas_position: {
width?: number;
height?: number;
left?: number;
top?: number;
};
image_url?: string;
origin_image_url?: string;
}
export interface WorkflowInfoResp {
role?: {
avatar?: {
image_uri?: string;
image_url?: string;
};
description?: string;
name?: string;
background_image_info?: {
web_background_image?: BackgroundImageResp;
mobile_background_image: BackgroundImageResp;
};
id: string;
connector_id: string;
suggest_reply_info?: {
suggest_reply_mode?: number;
customized_suggest_prompt?: string;
};
audio_config?: {
is_text_to_voice_enable?: boolean;
voice_config_map?: Record<
string,
{
voice_id: string;
name: string;
}
>;
};
workflow_id: string;
onboarding_info: {
prologue: string;
display_all_suggestions: boolean;
suggested_questions: string[];
};
user_input_config?: {
default_input_mode: number;
};
};
}
export type InitData = Pick<
MixInitResponse,
'prologue' | 'onboardingSuggestions' | 'backgroundInfo'
> & {
prologue: string;
onboardingSuggestions: {
id: string;
content: string;
}[];
botInfo: {
url: string;
nickname: string;
id: string;
};
displayAllSuggest?: boolean;
suggestPromoteInfo?: SuggestPromoteInfo;
defaultInputMode?: EInputMode;
customBgInfo?: {
imgUrl: string;
themeColor: string; // 背景颜色
};
};
export interface CozeApiFullFilledRes {
status: string;
reason: {
code: number;
};
data: unknown;
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { webSdkDefaultConnectorId, chatflowDraftConnectorId } from '@/util';
import { type IBuilderChatProps } from '../type';
export const getConnectorId = (props: IBuilderChatProps) => {
const { project } = props;
const { mode, connectorId } = project || {};
if (!connectorId) {
if (mode === 'websdk') {
return webSdkDefaultConnectorId;
} else if (mode === 'draft') {
return chatflowDraftConnectorId;
}
}
return connectorId;
};

View File

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

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo, useRef } from 'react';
import {
RequestScene,
type RequestManagerOptions,
} from '@coze-common/chat-core';
import { openApiHostByRegionWithToken } from '@/util/env';
import { type IBuilderChatProps } from '../type';
import { getConnectorId } from '../helper/get-connector-id';
import { useGetAppDataCombineWithProps } from '../context/builder-chat-context';
export const useCoreManager = (
props: IBuilderChatProps,
): RequestManagerOptions => {
const refProps = useRef(props);
const appData = useGetAppDataCombineWithProps();
const refAppData = useRef(appData);
refProps.current = props;
refAppData.current = appData;
return useMemo(
() => ({
scenes: {
[RequestScene.SendMessage]: {
hooks: {
onBeforeSendMessage: [
requestConfig => {
const { body } = requestConfig;
const bodyDataOld = JSON.parse(body);
const bodyData: Record<string, unknown> = {};
bodyData.additional_messages =
bodyDataOld.additional_messages || [];
bodyData.connector_id = bodyDataOld.connector_id;
bodyData.workflow_id = refProps?.current?.workflow?.id;
bodyData.parameters = refProps?.current?.workflow?.parameters;
bodyData.version =
refProps?.current?.project?.version || undefined;
bodyData.execute_mode =
refProps?.current?.project?.mode === 'draft'
? 'DEBUG'
: undefined;
bodyData.app_id =
refProps?.current?.project?.type === 'app'
? refProps?.current?.project?.id
: undefined;
bodyData.bot_id =
refProps?.current?.project?.type === 'bot'
? refProps?.current?.project?.id
: undefined;
bodyData.conversation_id = new URL(
requestConfig.url,
).searchParams.get('conversation_id');
bodyData.connector_id = getConnectorId(refProps?.current);
bodyData.ext = {
_caller: refProps?.current?.project?.caller,
user_id: bodyDataOld.user_id,
};
bodyData.suggest_reply_info = refAppData.current
?.suggestPromoteInfo
? {
suggest_reply_mode:
refAppData.current?.suggestPromoteInfo
?.suggestReplyMode,
customized_suggest_prompt:
refAppData.current?.suggestPromoteInfo
?.customizedSuggestPrompt,
}
: undefined;
requestConfig.body = JSON.stringify(bodyData);
requestConfig.url = `${openApiHostByRegionWithToken}/v1/workflows/chat`;
requestConfig.headers.push(
...Object.entries(refProps.current?.workflow?.header || {}),
);
return {
...requestConfig,
};
},
],
},
},
[RequestScene.ClearHistory]: {
hooks: {
onBeforeRequest: [
requestConfig => {
if (props?.project?.type === 'bot') {
requestConfig.data = {
connector_id: getConnectorId(props),
};
} else {
requestConfig.data = {
app_id: refProps?.current.project?.id,
conversation_name:
refProps?.current?.project?.conversationName,
get_or_create: false,
workflow_id: refProps?.current?.workflow?.id,
draft_mode: refProps?.current?.project?.mode === 'draft',
connector_id: getConnectorId(props),
};
}
return {
...requestConfig,
};
},
],
},
},
},
}),
[],
);
};

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useRef } from 'react';
import { type CozeAPI } from '@coze/api';
import { type OpenRequestInit } from '@/types/props';
import { type IBuilderChatProps } from '../type';
import { combineAppDataWithProps, getBotInfo } from '../services/get-bot-info';
import { createOrGetConversation } from '../services/create-conversation';
import { useSetAppDataFromOnLine } from '../context/builder-chat-context';
// conversationId、sectionId 重新修改
export const useRequestInit = (props: IBuilderChatProps) => {
const refProps = useRef(props);
refProps.current = props;
const setAppDataFromOnLine = useSetAppDataFromOnLine();
const openRequestInit = useCallback(
async (apiSdk?: CozeAPI): Promise<OpenRequestInit> => {
const getBotInfoPrm = getBotInfo(apiSdk || undefined, refProps.current);
const createOrGetConversationPrm = createOrGetConversation(
apiSdk || undefined,
refProps.current,
);
const botInfo = await getBotInfoPrm;
const conversationInfo = await createOrGetConversationPrm;
setAppDataFromOnLine?.(botInfo || null);
const formatAPPInfo = combineAppDataWithProps(botInfo, refProps.current);
return {
...formatAPPInfo,
...conversationInfo,
isCustomBackground: !!formatAPPInfo.customBgInfo?.imgUrl,
isBuilderChat: true,
};
},
[],
);
return openRequestInit;
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
PluginMode,
PluginName,
WriteableChatAreaPlugin,
createWriteableLifeCycleServices,
} from '@coze-common/chat-area';
import { type PluginBizContext } from './types/biz-context';
import { bizLifeCycleServiceGenerator } from './services/life-cycle';
export class BizPlugin extends WriteableChatAreaPlugin<PluginBizContext> {
/**
* 插件类型
* PluginMode.Readonly = 只读模式
* PluginMode.Writeable = 可写模式
*/
public pluginMode = PluginMode.Writeable;
/**
* 插件名称
* 请点 PluginName 里面去定义
*/
public pluginName = PluginName.UIBuilderEventcallbackPlugin;
/**
* 生命周期服务
*/
public lifeCycleServices = createWriteableLifeCycleServices(
this,
bizLifeCycleServiceGenerator,
);
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type WriteableMessageLifeCycleServiceGenerator } from '@coze-common/chat-area';
import { type PluginBizContext } from '../../types/biz-context';
export const messageLifeCycleServiceGenerator: WriteableMessageLifeCycleServiceGenerator<
PluginBizContext
> = plugin => {
let hasInit = false;
let lastMessageId = '';
let nowReceivingReplyId = '';
return {
onBeforeMessageGroupListUpdate: ctx => {
const { messages } =
plugin.chatAreaPluginContext.readonlyAPI.message.getMessagesStoreInstantValues();
const latestMessage = messages?.[0];
if (!hasInit || lastMessageId === latestMessage?.message_id) {
hasInit = true;
return ctx;
}
plugin.pluginBizContext.eventCallbacks?.onMessageChanged?.();
lastMessageId = latestMessage?.message_id;
if (
(latestMessage?.type === 'answer' &&
latestMessage?.is_finish === true) ||
!lastMessageId
) {
plugin.pluginBizContext?.eventCallbacks?.onMessageReceivedFinish?.();
}
return ctx;
},
onAfterSendMessage: ctx => {
const chatflowExecuteId: string =
// @ts-expect-error -- linter-disable-autofix, 新添加参数,接口未支持
ctx?.message?.extra_info?.chatflow_execute_id || '';
if (chatflowExecuteId) {
plugin.pluginBizContext?.eventCallbacks?.onGetChatFlowExecuteId?.(
chatflowExecuteId,
);
}
plugin.pluginBizContext?.eventCallbacks?.onMessageSended?.();
},
onBeforeReceiveMessage: ctx => {
if (nowReceivingReplyId === ctx.message.reply_id) {
return;
}
nowReceivingReplyId = ctx.message.reply_id;
plugin.pluginBizContext?.eventCallbacks?.onMessageReceivedStart?.();
},
};
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import i18n from '@coze-arch/i18n/intl';
import { type CozeAPI } from '@coze/api';
import { type IBuilderChatProps } from '../type';
import { getConnectorId } from '../helper/get-connector-id';
export const createOrGetConversation = async (
apiSdk: CozeAPI | undefined,
props: IBuilderChatProps,
) => {
let conversationId = '';
let sectionId = '';
try {
if (props?.project?.type === 'bot') {
const res = await apiSdk?.conversations.create(
{
// @ts-expect-error -- linter-disable-autofix
connector_id: getConnectorId(props),
},
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
);
conversationId = res?.id || '';
// @ts-expect-error -- linter-disable-autofix
sectionId = res.last_section_id;
} else {
if (IS_OPEN_SOURCE) {
const res = (await apiSdk?.post(
'/v1/workflow/conversation/create',
{
app_id: props.project?.id,
conversation_name: props?.project?.conversationName,
get_or_create: true,
draft_mode: props?.project?.mode === 'draft',
workflow_id: props?.workflow?.id,
connector_id: getConnectorId(props),
},
false,
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
)) as {
data: {
id: string;
last_section_id: string;
};
};
conversationId = res?.data?.id || '';
sectionId = res?.data?.last_section_id || '';
} else {
const res = await apiSdk?.conversations.create(
{
// @ts-expect-error -- linter-disable-autofix
app_id: props.project?.id,
conversation_name: props?.project?.conversationName,
get_or_create: true,
draft_mode: props?.project?.mode === 'draft',
workflow_id: props?.workflow?.id,
connector_id: getConnectorId(props),
},
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
);
conversationId = res?.id || '';
sectionId = res?.last_section_id || '';
}
}
return { conversationId, sectionId };
} catch (error) {
throw {
code: -1002,
message: '',
};
}
};

View File

@@ -0,0 +1,188 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cloneDeep } from 'lodash-es';
import i18n from '@coze-arch/i18n/intl';
import { type CozeAPI } from '@coze/api';
import { EInputMode } from '@/types/props';
import ChatFlowUserIcon from '@/assets/chatflow-logo.png';
import { type IBuilderChatProps } from '../type';
import { getConnectorId } from '../helper/get-connector-id';
import {
type ProjectInfoResp,
type WorkflowInfoResp,
type InitData,
type CozeApiFullFilledRes,
} from '../data-type';
const getFormatAppData = (
appData?: ProjectInfoResp['data'],
workflowData?: WorkflowInfoResp['role'],
props?: IBuilderChatProps,
): InitData => {
const appInfoResult: InitData = {
prologue: workflowData?.onboarding_info?.prologue || '',
onboardingSuggestions:
workflowData?.onboarding_info?.suggested_questions?.map(
(item, index) => ({
id: index.toString(),
content: item,
}),
) || [],
displayAllSuggest: workflowData?.onboarding_info?.display_all_suggestions,
botInfo: {
url: workflowData?.avatar?.image_url || appData?.icon_url || '',
nickname:
workflowData?.name || appData?.name || props?.project.defaultName || '',
id: props?.project?.id || '',
},
suggestPromoteInfo: {
suggestReplyMode: workflowData?.suggest_reply_info?.suggest_reply_mode,
customizedSuggestPrompt:
workflowData?.suggest_reply_info?.customized_suggest_prompt,
},
backgroundInfo: workflowData?.background_image_info,
defaultInputMode:
workflowData?.user_input_config?.default_input_mode === 2
? EInputMode.Voice
: EInputMode.Text,
};
// 内部插件中用的是origin_image_url但是这里origin_image_url 会过期因此使用image_url重写
if (appInfoResult.backgroundInfo?.web_background_image) {
appInfoResult.backgroundInfo.web_background_image.origin_image_url =
appInfoResult.backgroundInfo.web_background_image.image_url;
}
if (appInfoResult.backgroundInfo?.mobile_background_image) {
appInfoResult.backgroundInfo.mobile_background_image.origin_image_url =
appInfoResult.backgroundInfo.mobile_background_image.image_url;
}
return appInfoResult;
};
export const combineAppDataWithProps = (
appInfoResultRaw: InitData,
props?: IBuilderChatProps,
): InitData => {
const appInfoResult = cloneDeep(appInfoResultRaw);
if (props?.project?.id) {
appInfoResult.botInfo.id = props?.project?.id;
}
if (props?.project?.name) {
appInfoResult.botInfo.nickname = props?.project?.name;
}
if (props?.project?.iconUrl) {
appInfoResult.botInfo.url = props?.project?.iconUrl;
}
if (props?.project?.onBoarding?.prologue) {
appInfoResult.prologue = props?.project?.onBoarding?.prologue;
}
if (props?.project?.onBoarding?.suggestions?.length) {
appInfoResult.onboardingSuggestions =
props?.project?.onBoarding?.suggestions.map((item, index) => ({
id: index.toString(),
content: item,
})) || [];
}
if (props?.project?.onBoarding?.displayAllSuggest) {
appInfoResult.displayAllSuggest =
props?.project?.onBoarding?.displayAllSuggest;
}
if (!appInfoResult.displayAllSuggest) {
appInfoResult.onboardingSuggestions =
appInfoResult.onboardingSuggestions.slice(0, 3);
}
if (props?.project?.suggestPromoteInfo?.suggestReplyMode) {
appInfoResult.suggestPromoteInfo = {
suggestReplyMode: props?.project?.suggestPromoteInfo?.suggestReplyMode,
customizedSuggestPrompt:
props?.project?.suggestPromoteInfo?.customizedSuggestPrompt,
};
}
if (props?.areaUi?.bgInfo?.imgUrl) {
// 去掉backgroundInfo 使用本地写的背景组件,不再使用插件进行背景显示。
appInfoResult.customBgInfo = props?.areaUi?.bgInfo;
} else {
appInfoResult.customBgInfo = undefined;
}
return appInfoResult;
};
export const getBotInfo = async (
apiSdk: CozeAPI | undefined,
props: IBuilderChatProps,
) => {
const connectorId = getConnectorId(props);
const isWebSdk = props?.project?.mode === 'websdk';
const workflowId = props?.workflow?.id;
const isDebugParam = props?.project?.mode === 'draft' ? 'true' : '';
const callerParam = props?.project?.caller || '';
const lang = i18n.language;
console.log('i18n.language', lang);
const [appRes, workflowRes] = await Promise.allSettled([
isWebSdk
? apiSdk?.get<unknown, ProjectInfoResp>(
`/v1/apps/${props?.project?.id}?version=${props?.project?.version || ''}&connector_id=${connectorId}`,
{},
false,
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
)
: null,
workflowId
? apiSdk?.get<unknown, { data: WorkflowInfoResp }>(
`/v1/workflows/${workflowId}?${[
`connector_id=${connectorId}`,
`is_debug=${isDebugParam}`,
`caller=${callerParam}`,
].join('&')}`,
{},
false,
{
headers: {
'Accept-Language': i18n.language === 'zh-CN' ? 'zh' : 'en',
},
},
)
: null,
]);
const appData =
appRes?.status === 'fulfilled' ? appRes?.value?.data : undefined;
const workflowData =
workflowRes?.status === 'fulfilled'
? workflowRes?.value?.data?.role
: undefined;
if (isWebSdk && !appData) {
throw { code: (appRes as CozeApiFullFilledRes)?.reason?.code, message: '' };
}
const appInfo = getFormatAppData(appData, workflowData, props);
if (props?.workflow?.id && !appInfo.botInfo.url) {
appInfo.botInfo.url = ChatFlowUserIcon;
}
return appInfo;
};

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type React from 'react';
import { type ImageModel, type FileModel } from '@coze-common/chat-core';
import { type ContentType } from '@coze-common/chat-area';
import { type OpenUserInfo } from '@/types/user';
import { type DebugProps } from '@/types/props';
import {
type Layout,
type FooterConfig,
type HeaderConfig,
type FeedbackConfig,
type ConversationsConfig,
} from '@/types/client';
import { type OnImageClick } from '@/types/';
export interface IWorkflow {
id?: string;
parameters?: Record<string, unknown>;
header?: Record<string, string>;
}
export interface SuggestPromoteInfo {
customizedSuggestPrompt?: string;
suggestReplyMode?: number;
}
export interface IProject {
id: string;
type: 'app' | 'bot';
mode: 'draft' | 'release' | 'websdk' | 'audit'; // 草稿模式 | 发布模式 | webSdk发布
caller?: 'UI_BUILDER' | 'CANVAS';
connectorId?: string;
conversationName?: string; // project的话必须填写
conversationId?: string; // type 为bot的话必须填写
sectionId?: string; // type 为bot的话必须填写
name?: string;
desc?: string;
defaultName?: string;
defaultIconUrl?: string;
iconUrl?: string;
layout?: Layout;
version?: string;
onBoarding?: {
prologue?: string;
displayAllSuggest?: boolean;
suggestions?: string[];
};
suggestPromoteInfo?: SuggestPromoteInfo;
}
export interface IEventCallbacks {
onMessageChanged?: () => void;
onMessageSended?: () => void;
onMessageReceivedStart?: () => void;
onMessageReceivedFinish?: () => void;
onImageClick?: OnImageClick;
onGetChatFlowExecuteId?: (id: string) => void;
onThemeChange?: (theme: 'bg-theme' | 'light') => void;
}
export interface IBuilderChatProps {
workflow: IWorkflow;
project: IProject;
spaceId?: string;
eventCallbacks?: IEventCallbacks;
userInfo?: OpenUserInfo;
areaUi: {
isDisabled?: boolean; // 默认 false
uploadable?: boolean; // 默认 true
isNeedClearContext?: boolean; // 是否显示 clearContext按钮
isNeedClearMessage?: boolean; // 是否显示 clearMessage按钮
isNeedAddNewConversation?: boolean; //是否显示新增会话
isNeedFunctionCallMessage?: boolean;
isNeedQuote?: boolean;
feedback?: FeedbackConfig;
input?: {
placeholder?: string;
renderChatInputTopSlot?: (isChatError?: boolean) => React.ReactNode;
isShow?: boolean; //默认 true
defaultText?: string;
isNeedAudio?: boolean; // 是否需要语音输入默认是false
isNeedTaskMessage?: boolean;
};
header?: HeaderConfig & {
title?: string;
icon?: string;
}; // 默认是
footer?: FooterConfig;
conversations?: ConversationsConfig;
uiTheme?: 'uiBuilder' | 'chatFlow'; // uiBuilder 的主题
renderLoading?: () => React.ReactNode;
bgInfo?: {
imgUrl: string;
themeColor: string; // 背景颜色
};
};
auth?: {
type: 'external' | 'internal'; // 内部: cookie换token 外部: internal
token?: string;
refreshToken?: () => Promise<string> | string;
};
style?: React.CSSProperties;
debug?: DebugProps;
}
export type MessageType =
| {
type: ContentType.Text;
text: string;
}
| {
type: ContentType.Image;
value: ImageModel;
}
| {
type: ContentType.File;
value: FileModel;
};
export interface BuilderChatRef {
sendMessage: (message: MessageType) => void;
clearContext: () => void;
}

View File

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

View File

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

View File

@@ -1,170 +0,0 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@coze/chat-sdk/webCss';
import { forwardRef, useMemo, type Ref, useImperativeHandle } from 'react';
import {
openApiHostByRegionWithToken,
openApiCdnUrlByRegion,
} from '@coze-studio/open-env-adapter';
import { I18n } from '@coze-arch/i18n';
import ChatSdk from '@coze/chat-sdk/webJs';
import {
type RawMessage,
type IChatFlowProps,
type Language,
} from '@coze/chat-sdk';
import { type IBuilderChatProps, type BuilderChatRef } from '@/types';
const {
ChatFlowFramework,
ChatSlot,
useSendMessage,
ChatType,
RawMessageType,
} = ChatSdk;
export { ChatType, RawMessageType };
export const ChatContent = forwardRef(
(_props: {}, ref: Ref<BuilderChatRef>) => {
const { sendMessage } = useSendMessage();
useImperativeHandle(
ref,
() => ({
sendMessage: (message: RawMessage) => {
sendMessage(message);
},
}),
[sendMessage],
);
return <ChatSlot />;
},
);
export const BuilderChat = forwardRef(
(props: IBuilderChatProps, ref: Ref<BuilderChatRef>) => {
const { workflow } = props;
const eventCallbacks: IChatFlowProps['eventCallbacks'] = useMemo(
() => ({
onImageClick: props.eventCallbacks?.onImageClick,
onGetChatFlowExecuteId: props.eventCallbacks?.onGetChatFlowExecuteId,
onThemeChange: props.eventCallbacks?.onThemeChange,
onInitSuccess: props.eventCallbacks?.onInitSuccess,
message: {
afterMessageReceivedFinish:
props.eventCallbacks?.afterMessageReceivedFinish,
},
}),
[props.eventCallbacks],
);
const { userInfo } = props;
const { auth } = props;
const areaUi: IChatFlowProps['areaUi'] = useMemo(
// eslint-disable-next-line complexity
() => ({
layout: props.project?.layout,
isDisabled: props.areaUi?.isDisabled,
input: {
isNeed: props.areaUi?.input?.isShow,
isNeedAudio: props.areaUi?.input?.isNeedAudio ?? !IS_OVERSEA,
placholder: props.areaUi?.input?.placeholder,
isNeedTaskMessage: props.areaUi?.input?.isNeedTaskMessage,
defaultText: props.areaUi?.input?.defaultText,
renderChatInputTopSlot: props.areaUi?.input?.renderChatInputTopSlot,
},
clearContext:
props.project?.mode === 'websdk'
? {
isNeed: true,
position: 'inputLeft',
}
: {
isNeed: false,
},
clearMessage:
props.project?.mode === 'websdk'
? {
isNeed: true,
position: 'headerRight',
}
: {
isNeed:
props.areaUi?.isNeedClearMessage !== undefined
? props.areaUi?.isNeedClearMessage
: true,
position: 'inputLeft',
},
uploadBtn: {
isNeed: props.areaUi?.uploadable,
},
uiTheme: props.areaUi?.uiTheme,
renderLoading: props.areaUi?.renderLoading,
header: {
isNeed: props.areaUi?.header?.isShow || false,
icon: props.project?.iconUrl,
title: props.project?.name,
renderRightSlot: () => <>{props.areaUi?.header?.extra || null}</>,
},
footer: props.areaUi?.footer,
}),
[props.areaUi, props.project],
);
const setting: IChatFlowProps['setting'] = useMemo(
() => ({
apiBaseUrl: openApiHostByRegionWithToken,
cdnBaseUrlPath: openApiCdnUrlByRegion,
language: I18n.language as Language,
logLevel: IS_BOE ? 'debug' : 'release',
...(props.setting || {}),
}),
[],
);
const project: IChatFlowProps['project'] = useMemo(
() => ({
id: props.project?.id || '',
type: props.project?.type,
mode: props.project?.mode as 'release',
caller: props.project?.caller,
defaultName: props.project?.defaultName,
defaultIconUrl: props.project?.defaultIconUrl,
connectorId: props.project?.connectorId,
conversationName: props.project?.conversationName,
name: props.project?.name,
iconUrl: props.project?.iconUrl,
OnBoarding: props.project?.onBoarding,
}),
[props.project],
);
return (
<>
<ChatFlowFramework
workflow={workflow}
project={project}
userInfo={userInfo}
eventCallbacks={eventCallbacks}
auth={auth}
style={props.style}
areaUi={areaUi}
setting={setting}
>
<ChatContent ref={ref} />
</ChatFlowFramework>
</>
);
},
);

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