feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
import { mergeConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
viteFinal: config =>
mergeConfig(config, {
plugins: [
svgr({
svgrOptions: {
native: false,
},
}),
],
}),
};
export default config;

View File

@@ -0,0 +1,14 @@
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# @coze-common/chat-answer-action
Chat 消息底部的操作按钮功能
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

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 { getIsLastGroup } from '../src/utils/get-is-last-group';
it('getIsLastGroupCorrectly', () => {
const res1 = getIsLastGroup({
meta: { isFromLatestGroup: false, sectionId: '123' },
latestSectionId: '123',
});
const res2 = getIsLastGroup({
meta: { isFromLatestGroup: true, sectionId: '123' },
latestSectionId: '123',
});
const res3 = getIsLastGroup({
meta: { isFromLatestGroup: true, sectionId: '321' },
latestSectionId: '123',
});
const res4 = getIsLastGroup({
meta: { isFromLatestGroup: false, sectionId: '321' },
latestSectionId: '123',
});
expect(res1).toBeFalsy();
expect(res2).toBeTruthy();
expect(res3).toBeFalsy();
expect(res4).toBeFalsy();
});

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getIsPushedMessage } from '../src/utils/get-is-pushed-message';
vi.mock('@coze-common/chat-area', () => ({
getIsTriggerMessage: (param: any) =>
param.type === 'task_manual_trigger' || param.source === 1,
getIsNotificationMessage: (param: any) => param.source === 2,
getIsAsyncResultMessage: (param: any) => param.source === 3,
}));
it('getIsPushedMessageCorrectly', () => {
const res1 = getIsPushedMessage({ type: 'answer', source: 0 });
const res2 = getIsPushedMessage({ type: 'answer', source: 3 });
const res3 = getIsPushedMessage({ type: 'answer', source: 1 });
const res4 = getIsPushedMessage({ type: 'task_manual_trigger', source: 0 });
const res5 = getIsPushedMessage({ type: 'answer', source: 2 });
expect(res1).toBeFalsy();
expect(res2).toBeTruthy();
expect(res3).toBeTruthy();
expect(res4).toBeTruthy();
expect(res5).toBeTruthy();
});

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getShowFeedback } from '../src/utils/get-show-feedback';
vi.mock('@coze-common/chat-area', () => ({
getIsTriggerMessage: (param: any) =>
param.type === 'task_manual_trigger' || param.source === 1,
getIsNotificationMessage: (param: any) => param.source === 2,
getIsAsyncResultMessage: (param: any) => param.source === 3,
}));
it('getShowFeedbackCorrectly', () => {
const res1 = getShowFeedback({
message: { type: 'answer', source: 0 },
meta: {
isFromLatestGroup: true,
sectionId: '123',
isGroupLastAnswerMessage: true,
},
latestSectionId: '123',
});
const res2 = getShowFeedback({
message: { type: 'ack', source: 0 },
meta: {
isFromLatestGroup: true,
sectionId: '123',
isGroupLastAnswerMessage: false,
},
latestSectionId: '123',
});
const res3 = getShowFeedback({
message: { type: 'answer', source: 0 },
meta: {
isFromLatestGroup: true,
sectionId: '123',
isGroupLastAnswerMessage: true,
},
latestSectionId: '321',
});
const res4 = getShowFeedback({
message: { type: 'task_manual_trigger', source: 0 },
meta: {
isFromLatestGroup: true,
sectionId: '321',
isGroupLastAnswerMessage: true,
},
latestSectionId: '321',
});
const res5 = getShowFeedback({
message: { type: 'answer', source: 2 },
meta: {
isFromLatestGroup: false,
sectionId: '321',
isGroupLastAnswerMessage: true,
},
latestSectionId: '321',
});
const res6 = getShowFeedback({
message: { type: 'answer', source: 0 },
meta: {
isFromLatestGroup: true,
sectionId: '123',
isGroupLastAnswerMessage: false,
},
latestSectionId: '123',
});
expect(res1).toBeTruthy();
expect(res2).toBeFalsy();
expect(res3).toBeFalsy();
expect(res4).toBeFalsy();
expect(res5).toBeFalsy();
expect(res6).toBeFalsy();
});

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 { getShowRegenerate } from '../src/utils/get-show-regenerate';
vi.mock('@coze-common/chat-area', () => ({
getIsTriggerMessage: (param: any) =>
param.type === 'task_manual_trigger' || param.source === 1,
getIsNotificationMessage: (param: any) => param.source === 2,
getIsAsyncResultMessage: (param: any) => param.source === 3,
}));
it('getShowRegenerateCorrectly', () => {
const res1 = getShowRegenerate({
message: { type: 'answer', source: 0 },
meta: { isFromLatestGroup: true, sectionId: '123' },
latestSectionId: '123',
});
const res2 = getShowRegenerate({
message: { type: 'ack', source: 0 },
meta: { isFromLatestGroup: true, sectionId: '123' },
latestSectionId: '123',
});
const res3 = getShowRegenerate({
message: { type: 'answer', source: 0 },
meta: { isFromLatestGroup: true, sectionId: '123' },
latestSectionId: '321',
});
const res4 = getShowRegenerate({
message: { type: 'task_manual_trigger', source: 0 },
meta: { isFromLatestGroup: true, sectionId: '321' },
latestSectionId: '321',
});
const res5 = getShowRegenerate({
message: { type: 'answer', source: 2 },
meta: { isFromLatestGroup: false, sectionId: '321' },
latestSectionId: '321',
});
expect(res1).toBeTruthy();
expect(res2).toBeTruthy();
expect(res3).toBeFalsy();
expect(res4).toBeFalsy();
expect(res5).toBeFalsy();
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,85 @@
{
"name": "@coze-common/chat-answer-action",
"version": "0.0.1",
"description": "Chat Answer Action",
"license": "Apache-2.0",
"author": "gaoyuanhan.duty@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.ts",
"./hooks/*": "./src/hooks/*"
},
"main": "src/index.ts",
"typesVersions": {
"*": {
"hooks/*": [
"./src/hooks/*"
]
}
},
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-flags": "workspace:*",
"@coze-common/chat-area-plugin-message-grab": "workspace:*",
"@coze-common/chat-uikit": "workspace:*",
"@coze-common/chat-uikit-shared": "workspace:*",
"@coze-common/text-grab": "workspace:*",
"classnames": "^2.3.2",
"immer": "^10.0.3"
},
"devDependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-icons": "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/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@coze-common/chat-area": "workspace:*",
"@coze-common/chat-area-utils": "workspace:*",
"@coze-common/chat-core": "workspace:*",
"@douyinfe/semi-icons": "^2.36.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/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"ahooks": "^3.7.8",
"copy-to-clipboard": "^3.3.3",
"lodash-es": "^4.17.21",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"stylelint": "^15.11.0",
"vite": "^4.3.9",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5",
"zustand": "^4.4.7"
},
"peerDependencies": {
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-common/chat-area": "workspace:*",
"@coze-common/chat-area-utils": "workspace:*",
"@coze-common/chat-core": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"ahooks": "^3.7.8",
"copy-to-clipboard": "^3.3.3",
"react": ">=18.2.0",
"react-dom": ">=18.2.0",
"zustand": "^4.4.7"
}
}

View File

@@ -0,0 +1,29 @@
.container {
display: flex;
flex-direction: column;
.icon-container {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 4px;
.left-content {
display: flex;
flex-shrink: 0;
gap: 4px;
align-items: center;
}
.right-content {
display: flex;
gap: 4px;
align-items: center;
margin-left: 8px;
}
}
}

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 PropsWithChildren } from 'react';
import s from './index.module.less';
interface ActionBarContainerProps {
leftContent?: React.ReactNode;
rightContent?: React.ReactNode;
}
export const ActionBarContainer: React.FC<
PropsWithChildren<ActionBarContainerProps>
> = ({ leftContent, rightContent, children }) => (
<div className={s.container}>
<div className={s['icon-container']}>
<div
data-testid="chat-area.answer-action.left-content"
className={s['left-content']}
>
{leftContent}
</div>
<div
data-testid="chat-area.answer-action.right-content"
className={s['right-content']}
>
{rightContent}
</div>
</div>
{children}
</div>
);
ActionBarContainer.displayName = 'ActionBarContainer';

View File

@@ -0,0 +1,14 @@
.container {
display: flex;
gap: 4px;
align-items: center;
width: 100%;
height: 32px;
padding: 4px;
border-style: solid;
border-width: 1px;
border-radius: 8px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 10%);
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren } from 'react';
import classNames from 'classnames';
import s from './index.module.less';
// TODO 后续迭代扩展时props可细化
interface ActionBarHoverContainerProps {
style?: React.CSSProperties;
}
export const ActionBarHoverContainer: React.FC<
PropsWithChildren<ActionBarHoverContainerProps>
> = ({ children, style }) => (
<div
data-testid="chat-area.answer-action.hover-action-bar"
className={classNames(s.container, ['coz-stroke-primary', 'coz-bg-max'])}
style={style}
>
{children}
</div>
);

View File

@@ -0,0 +1,15 @@
@keyframes trigger-config-button-slider-in {
from {
transform: translateX(30px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in {
animation: trigger-config-button-slider-in 0.5s linear;
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 } from 'react';
import classNames from 'classnames';
import {
useLatestSectionId,
useMessageBoxContext,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { TriggerEnabled } from '@coze-arch/bot-api/developer_api';
import { getShowBotTriggerButton } from '../../utils/get-show-bot-trigger-button';
import { useUpdateHomeTriggerConfig } from '../../hooks/use-update-home-trigger-config';
import { useGetBotParticipantInfo } from '../../hooks/use-get-bot-participant-info';
import { useAnswerActionStore } from '../../context/store';
import { useAnswerActionPreference } from '../../context/preference';
import styles from './index.module.less';
export interface BotTriggerConfigButtonGroupProps {
addonBefore?: ReactNode;
}
export const BotTriggerConfigButtonGroup: React.FC<
BotTriggerConfigButtonGroupProps
> = ({ addonBefore }) => {
const { message, meta } = useMessageBoxContext();
const { sender_id } = message;
const { useFavoriteBotTriggerConfigStore } = useAnswerActionStore();
const botParticipantInfo = useFavoriteBotTriggerConfigStore(
state => state.favoriteBotTriggerConfigMap[sender_id ?? ''],
);
const latestSectionId = useLatestSectionId();
const isShowTriggerButton = getShowBotTriggerButton({
message,
meta,
latestSectionId,
});
const { enableBotTriggerControl } = useAnswerActionPreference();
useGetBotParticipantInfo({
botId: sender_id,
isEnabled:
isShowTriggerButton && !botParticipantInfo && enableBotTriggerControl,
});
const { keepReceiveHomeTrigger, stopReceiveHomeTrigger, loading } =
useUpdateHomeTriggerConfig({
botId: sender_id,
onSuccess: isKeepReceiveTrigger => {
if (!sender_id) {
return;
}
const { updateFavoriteBotTriggerConfigMapByImmer } =
useFavoriteBotTriggerConfigStore.getState();
updateFavoriteBotTriggerConfigMapByImmer(draft => {
const targetConfig = draft[sender_id];
if (!targetConfig) {
return;
}
targetConfig.trigger_enabled = isKeepReceiveTrigger
? TriggerEnabled.Open
: TriggerEnabled.Close;
});
},
});
if (!isShowTriggerButton) {
return null;
}
if (!botParticipantInfo) {
return null;
}
const { is_store_favorite, trigger_enabled } = botParticipantInfo;
if (!enableBotTriggerControl) {
return null;
}
if (!is_store_favorite || trigger_enabled !== TriggerEnabled.Init) {
return null;
}
return (
<div
className={classNames(
styles['slide-in'],
'flex gap-x-[8px] items-center',
)}
>
{addonBefore}
<div className="flex gap-x-[6px] items-center">
<Button
color="highlight"
onClick={stopReceiveHomeTrigger}
loading={loading}
size="small"
>
{I18n.t('stop_receiving')}
</Button>
<Button
color="brand"
onClick={keepReceiveHomeTrigger}
loading={loading}
size="small"
>
{I18n.t('keep')}
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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, useState, type PropsWithChildren } from 'react';
import copy from 'copy-to-clipboard';
import classNames from 'classnames';
import { getReportError } from '@coze-common/chat-area-utils';
import {
getIsTextMessage,
useChatArea,
useMessageBoxContext,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import { IconCozCheckMark, IconCozCopy } from '@coze-arch/coze-design/icons';
import { IconButton, Toast, Tooltip } from '@coze-arch/coze-design';
import { ReportEventNames } from '../../report-events';
import { useTooltipTrigger } from '../../hooks/use-tooltip-trigger';
type CopyTextMessageProps = Omit<
ComponentProps<typeof IconButton>,
'icon' | 'iconSize' | 'onClick'
>;
export const CopyTextMessage: React.FC<
PropsWithChildren<CopyTextMessageProps>
> = ({ className, ...props }) => {
const { reporter } = useChatArea();
const { message, meta } = useMessageBoxContext();
const { content } = message;
const [isCopySuccessful, setIsCopySuccessful] = useState<boolean>(false);
const trigger = useTooltipTrigger('hover');
// 单位s
const COUNT_DOWN_TIME = 3;
// 单位s转化为ms的倍数
const TIMES = 1000;
const handleCopy = () => {
const resp = copy(content);
if (resp) {
// 复制成功
setIsCopySuccessful(true);
setTimeout(() => setIsCopySuccessful(false), COUNT_DOWN_TIME * TIMES);
Toast.success({
content: I18n.t('copy_success'),
showClose: false,
duration: COUNT_DOWN_TIME,
});
reporter.successEvent({
eventName: ReportEventNames.CopyTextMessage,
});
} else {
// 复制失败
Toast.warning({
content: I18n.t('copy_failed'),
showClose: false,
duration: COUNT_DOWN_TIME,
});
reporter.errorEvent({
eventName: ReportEventNames.CopyTextMessage,
...getReportError('copy_text_message_error', 'copy_text_message_error'),
});
}
};
const isTextMessage = getIsTextMessage(message);
if (!isTextMessage) {
return null;
}
if (!meta.isGroupLastAnswerMessage) {
return null;
}
const iconClassNames = classNames(className, 'w-[14px] h-[14px]');
return (
<Tooltip
content={isCopySuccessful ? I18n.t('copied') : I18n.t('copy')}
trigger={trigger}
>
<IconButton
data-testid="chat-area.answer-action.copy-button"
size="small"
icon={
isCopySuccessful ? (
<IconCozCheckMark className={iconClassNames} />
) : (
<IconCozCopy className={iconClassNames} />
)
}
color="secondary"
onClick={handleCopy}
{...props}
/>
</Tooltip>
);
};

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 PropsWithChildren } from 'react';
import classNames from 'classnames';
import {
useDeleteMessageGroup,
useIsDeleteMessageLock,
useMessageBoxContext,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import { IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import { useTooltipTrigger } from '../../hooks/use-tooltip-trigger';
type DeleteMessageProps = Omit<
ComponentProps<typeof IconButton>,
'icon' | 'iconSize' | 'onClick'
>;
export const DeleteMessage: React.FC<PropsWithChildren<DeleteMessageProps>> = ({
className,
...props
}) => {
const { groupId } = useMessageBoxContext();
const trigger = useTooltipTrigger('hover');
const isDeleteMessageLock = useIsDeleteMessageLock(groupId);
const deleteMessageGroup = useDeleteMessageGroup();
return (
<Tooltip trigger={trigger} content={I18n.t('Delete')}>
<IconButton
data-testid="chat-area.answer-action.delete-message-button"
disabled={isDeleteMessageLock}
size="small"
icon={
<IconCozTrashCan
className={classNames(
'coz-fg-hglt-red',
className,
'w-[14px] h-[14px]',
)}
/>
}
onClick={() => {
// 通过 groupId 索引即可
deleteMessageGroup(groupId);
}}
color="secondary"
{...props}
/>
</Tooltip>
);
};

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 classNames from 'classnames';
export interface AnswerActionDividerProps {
className?: string;
}
export const AnswerActionDivider: React.FC<AnswerActionDividerProps> = ({
className,
}) => (
<div
className={classNames(
'h-[12px] border-solid border-0 border-l-[1px] coz-stroke-primary mx-[8px]',
className,
)}
/>
);

View File

@@ -0,0 +1,63 @@
.frown-upon-container {
display: flex;
flex-direction: column;
width: 100%;
margin-top: 12px;
padding: 12px;
border: 1px solid rgba(10, 17, 61, 6%);
.header {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 12px;
line-height: 20px;
.title {
font-size: 14px;
font-weight: 600;
font-style: normal;
color: rgba(6, 7, 9, 96%);
}
}
.reasons {
display: flex;
gap: 8px;
margin-bottom: 8px;
.item {
cursor: pointer;
padding: 8px 16px;
font-size: 14px;
font-weight: 400;
font-style: normal;
background-color: rgba(6, 7, 9, 5%);
border-radius: 6px;
}
.item:hover {
background-color: rgba(128, 138, 255, 20%);
}
.item.selected {
background-color: rgba(128, 138, 255, 40%);
}
}
.textarea {
margin-bottom: 12px;
}
.footer {
text-align: right;
}
}

View File

@@ -0,0 +1,283 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 CSSProperties,
useRef,
useState,
type PropsWithChildren,
} from 'react';
import classNames from 'classnames';
import { useHover } from 'ahooks';
import {
MessageFeedbackDetailType,
MessageFeedbackType,
} from '@coze-common/chat-core';
import {
useChatAreaLayout,
useLatestSectionId,
useMessageBoxContext,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import {
IconCozThumbdown,
IconCozThumbdownFill,
IconCozCross,
} from '@coze-arch/coze-design/icons';
import { TextArea, Tooltip, Button, IconButton } from '@coze-arch/coze-design';
import { Layout } from '@coze-common/chat-uikit-shared';
import { getShowFeedback } from '../../utils/get-show-feedback';
import { useReportMessageFeedback } from '../../hooks/use-report-message-feedback';
import { useDispatchMouseLeave } from '../../hooks/use-dispatch-mouse-leave';
import s from './index.module.less';
export interface FrownUponProps {
onClick?: () => void;
isFrownUponPanelVisible: boolean;
isFrownUponSuccessful: boolean;
}
export interface FrownUponUIProps extends FrownUponProps {
isMobile: boolean;
}
// 点踩按钮
export const FrownUpon: React.FC<PropsWithChildren<FrownUponProps>> = ({
onClick,
isFrownUponPanelVisible,
isFrownUponSuccessful,
}) => {
const layout = useChatAreaLayout();
const isMobileLayout = layout === Layout.MOBILE;
const reportMessageFeedback = useReportMessageFeedback();
const { message, meta } = useMessageBoxContext();
const latestSectionId = useLatestSectionId();
const handleClick = () => {
reportMessageFeedback({
message_feedback: {
feedback_type: isFrownUponSuccessful
? MessageFeedbackType.Default
: MessageFeedbackType.Unlike,
detail_types: isFrownUponSuccessful
? []
: [MessageFeedbackDetailType.UnlikeDefault],
},
}).then(() => {
// 接口调用后再切换展示状态
onClick?.();
});
};
if (!getShowFeedback({ message, meta, latestSectionId })) {
return null;
}
return (
<FrownUponUI
onClick={handleClick}
isMobile={isMobileLayout}
isFrownUponPanelVisible={isFrownUponPanelVisible}
isFrownUponSuccessful={isFrownUponSuccessful}
/>
);
};
export const FrownUponUI: React.FC<FrownUponUIProps> = ({
onClick,
isFrownUponPanelVisible,
isFrownUponSuccessful,
isMobile,
}) => {
const toolTipWrapperRef = useRef<HTMLDivElement>(null);
const isHovering = useHover(toolTipWrapperRef);
// 解决点踩填写原因面板展开收起过程中点踩按钮的tooltip展示错乱问题
useDispatchMouseLeave(toolTipWrapperRef, isFrownUponPanelVisible);
return (
<div style={{ position: 'relative' }} ref={toolTipWrapperRef}>
<Tooltip
content={I18n.t('dislike')}
getPopupContainer={() => toolTipWrapperRef.current ?? document.body}
trigger="custom"
visible={!isMobile && isHovering}
>
<IconButton
data-testid="chat-area.answer-action.frown-upon-buton"
size="small"
icon={
isFrownUponSuccessful ? (
<IconCozThumbdownFill className="w-[14px] h-[14px]" />
) : (
<IconCozThumbdown className="w-[14px] h-[14px]" />
)
}
color="secondary"
onClick={onClick}
/>
</Tooltip>
</div>
);
};
const getFrownUponPanelReasons = () => [
{
label: I18n.t('dislike_feedback_tag_harm'),
value: MessageFeedbackDetailType.UnlikeHarmful,
},
{
label: I18n.t('dislike_feedback_tag_mislead'),
value: MessageFeedbackDetailType.UnlikeIncorrect,
},
{
label: I18n.t('dislike_feedback_tag_unfollow_instruction'),
value: MessageFeedbackDetailType.UnlikeNotFollowInstructions,
},
{
label: I18n.t('dislike_feedback_tag_unfollow_others'),
value: MessageFeedbackDetailType.UnlikeOthers,
},
];
export interface FrownUponPanelProps {
containerStyle?: CSSProperties;
onCancel?: () => void;
onSubmit?: () => void;
wrapReasons?: boolean;
}
export interface OnFrownUponSubmitParam {
reasons: Array<MessageFeedbackDetailType>;
detailContent: string;
}
export interface FrownUponPanelUIProps {
onSubmit: (param: OnFrownUponSubmitParam) => void;
onCancel: (() => void) | undefined;
wrapReasons: boolean | undefined;
style?: CSSProperties;
}
// 点踩填写原因面板
export const FrownUponPanel: React.FC<
PropsWithChildren<FrownUponPanelProps>
> = ({ containerStyle, onCancel, onSubmit, wrapReasons }) => {
const reportMessageFeedback = useReportMessageFeedback();
const handleSubmit = ({ reasons, detailContent }: OnFrownUponSubmitParam) => {
reportMessageFeedback({
message_feedback: {
feedback_type: MessageFeedbackType.Unlike,
detail_types:
reasons.length > 0
? reasons
: [MessageFeedbackDetailType.UnlikeDefault],
detail_content: reasons.includes(MessageFeedbackDetailType.UnlikeOthers)
? detailContent
: undefined,
},
}).then(() => {
// 接口调用后再切换展示状态
onSubmit?.();
});
};
return (
<FrownUponPanelUI
wrapReasons={wrapReasons}
onSubmit={handleSubmit}
onCancel={onCancel}
style={containerStyle}
/>
);
};
export const FrownUponPanelUI: React.FC<FrownUponPanelUIProps> = ({
onSubmit,
onCancel,
style,
wrapReasons,
}) => {
const [reasons, setReasons] = useState<Array<MessageFeedbackDetailType>>([]);
const [textAreaValue, setTextAreaValue] = useState<string>('');
const handleItemClick = (reason: MessageFeedbackDetailType) => {
setReasons(prev => {
if (prev.includes(reason)) {
return prev.filter(item => item !== reason);
}
return [...prev, reason];
});
};
return (
<div
className={classNames(s['frown-upon-container'], [
'bg-[var(--coz-mg-card)]',
'rounded-normal',
])}
style={style}
>
<div className={s.header}>
<div className={s.title}>{I18n.t('dislike_feedback_title')}</div>
<IconButton
size="small"
color="secondary"
icon={<IconCozCross />}
onClick={onCancel}
/>
</div>
<div className={classNames(s.reasons, wrapReasons && 'flex-wrap')}>
{getFrownUponPanelReasons().map(item => (
<span
className={classNames(s.item, {
[`${s.selected}`]: reasons.includes(item.value),
})}
onClick={() => {
handleItemClick(item.value);
}}
>
{item.label}
</span>
))}
</div>
<div className={s.textarea}>
{reasons.includes(MessageFeedbackDetailType.UnlikeOthers) && (
<TextArea
placeholder={I18n.t('dislike_feedback_placeholder')}
maxCount={500}
showClear
rows={2}
value={textAreaValue}
onChange={v => {
setTextAreaValue(v);
}}
/>
)}
</div>
<div className={s.footer}>
<Button
theme="solid"
onClick={() => onSubmit({ reasons, detailContent: textAreaValue })}
>
{I18n.t('feedback_submit')}
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 classNames from 'classnames';
import {
useDeleteMessageGroup,
useIsDeleteMessageLock,
useMessageBoxContext,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import { IconCozMore, IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { IconButton, Dropdown } from '@coze-arch/coze-design';
interface MoreOperationsProps {
className?: string;
}
export const MoreOperations: React.FC<MoreOperationsProps> = ({
className,
}) => {
const { groupId } = useMessageBoxContext();
const isDeleteMessageLock = useIsDeleteMessageLock(groupId);
const deleteMessageGroup = useDeleteMessageGroup();
return (
<Dropdown
render={
<Dropdown.Menu mode="menu">
<Dropdown.Item
disabled={isDeleteMessageLock}
icon={<IconCozTrashCan className="coz-fg-hglt-red" />}
onClick={() => {
// 通过 groupId 索引即可
deleteMessageGroup(groupId);
}}
type="danger"
>
{I18n.t('Delete')}
</Dropdown.Item>
</Dropdown.Menu>
}
>
<IconButton
data-testid="chat-area.answer-action.more-operation-button"
size="small"
color="secondary"
icon={
<IconCozMore className={classNames(className, 'w-[14px] h-[14px]')} />
}
/>
</Dropdown>
);
};

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 PropsWithChildren } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import {
parseMarkdownToGrabNode,
GrabElementType,
} from '@coze-common/text-grab';
import { messageSource, type MessageContent } from '@coze-common/chat-core';
import {
safeAsyncThrow,
typeSafeJsonParseEnhanced,
} from '@coze-common/chat-area-utils';
import { type GrabPluginBizContext } from '@coze-common/chat-area-plugin-message-grab';
import {
getIsImageMessage,
ContentType,
getIsTextMessage,
useMessageBoxContext,
type WriteableChatAreaPlugin,
} from '@coze-common/chat-area';
import { IconCozQuotation } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import { useTooltipTrigger } from '../../hooks/use-tooltip-trigger';
import { useQuotePlugin } from '../../hooks/use-quote-plugin';
type QuoteMessageProps = Omit<
ComponentProps<typeof IconButton>,
'icon' | 'iconSize' | 'onClick'
>;
export const QuoteMessage: React.FC<
PropsWithChildren<QuoteMessageProps>
> = props => {
const plugin = useQuotePlugin();
const { message } = useMessageBoxContext();
if (!plugin || message.source === messageSource.Notice) {
return null;
}
return <QuoteMessageImpl {...props} />;
};
/**
* 哥哥们改动这里要小心一点喔QuoteMessageImpl的前置依赖项是 message-grab
*/
export const QuoteMessageImpl: React.FC<
PropsWithChildren<QuoteMessageProps>
> = ({ className, ...props }) => {
// INFO: 这里使用 as 是因为明确的知道 父组件提前尝试取 plugin 并且提前拦截的情况
// 后续如果有改动,请务必注意这里
const plugin = useQuotePlugin() as WriteableChatAreaPlugin<
GrabPluginBizContext,
unknown
>;
const { chatAreaPluginContext, pluginBizContext } = plugin;
const { useDeleteFile } = plugin.chatAreaPluginContext.writeableHook.file;
const { getFileStoreInstantValues } =
chatAreaPluginContext.readonlyAPI.batchFile;
const { useQuoteStore } = pluginBizContext.storeSet;
const { onQuote } = pluginBizContext.eventCallbacks;
const { updateQuoteContent, updateQuoteVisible } = useQuoteStore.getState();
const { message, meta } = useMessageBoxContext();
const { content, content_type } = message;
const trigger = useTooltipTrigger('hover');
const deleteFile = useDeleteFile();
const deleteAllFile = () => {
const { fileIdList } = getFileStoreInstantValues();
fileIdList.forEach(id => deleteFile(id));
};
const handleQuote = () => {
deleteAllFile();
if (content_type === ContentType.Image) {
const contentObj = typeSafeJsonParseEnhanced<
MessageContent<ContentType.Image>
>({
str: content,
verifyStruct: (
_content: unknown,
): _content is MessageContent<ContentType.Image> =>
_content instanceof Object && 'image_list' in _content,
onParseError: error => {
safeAsyncThrow(error.message);
},
onVerifyError: error => {
safeAsyncThrow(error.message);
},
});
updateQuoteContent(
contentObj?.image_list.map(img => ({
type: GrabElementType.IMAGE,
src: img.image_ori.url,
children: [],
})) ?? [],
);
} else {
const grabNodeList = parseMarkdownToGrabNode(content);
updateQuoteContent(grabNodeList);
}
onQuote?.({ botId: message.sender_id ?? '', source: message.source });
updateQuoteVisible(true);
};
const isTextMessage = getIsTextMessage(message);
const isImageMessage = getIsImageMessage(message);
if (!isTextMessage && !isImageMessage) {
return null;
}
if (!meta.isGroupLastAnswerMessage) {
return null;
}
if (!plugin) {
return null;
}
return (
<Tooltip content={I18n.t('quote_ask_in_chat')} trigger={trigger}>
<IconButton
data-testid="chat-area.answer-action.quote-message"
size="small"
icon={
<IconCozQuotation
className={classNames(className, 'w-[14px] h-[14px]')}
/>
}
onClick={handleQuote}
color="secondary"
{...props}
/>
</Tooltip>
);
};
QuoteMessage.displayName = 'QuoteMessage';

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 ComponentProps, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import {
useMessageBoxContext,
useLatestSectionId,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import { IconCozRefresh } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import { getShowRegenerate } from '../../utils/get-show-regenerate';
import { useTooltipTrigger } from '../../hooks/use-tooltip-trigger';
type RegenerateMessageProps = Omit<
ComponentProps<typeof IconButton>,
'icon' | 'iconSize' | 'onClick'
>;
export const RegenerateMessage: React.FC<
PropsWithChildren<RegenerateMessageProps>
> = ({ className, ...props }) => {
const { message, meta, regenerateMessage } = useMessageBoxContext();
const latestSectionId = useLatestSectionId();
const trigger = useTooltipTrigger('hover');
const showRegenerate = getShowRegenerate({ message, meta, latestSectionId });
if (!showRegenerate) {
return null;
}
return (
<Tooltip trigger={trigger} content={I18n.t('message_tool_regenerate')}>
<IconButton
data-testid="chat-area.answer-action.regenerate-message-button"
size="small"
color="secondary"
icon={
<IconCozRefresh
className={classNames(className, 'w-[14px] h-[14px]')}
/>
}
onClick={() => {
regenerateMessage();
}}
{...props}
/>
</Tooltip>
);
};

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 { useRef } from 'react';
import { useHover } from 'ahooks';
import { MessageFeedbackType } from '@coze-common/chat-core';
import {
useChatAreaLayout,
useLatestSectionId,
useMessageBoxContext,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import {
IconCozThumbsup,
IconCozThumbsupFill,
} from '@coze-arch/coze-design/icons';
import { Tooltip, IconButton } from '@coze-arch/coze-design';
import { Layout } from '@coze-common/chat-uikit-shared';
import { getShowFeedback } from '../../utils/get-show-feedback';
import { useReportMessageFeedback } from '../../hooks/use-report-message-feedback';
import { useDispatchMouseLeave } from '../../hooks/use-dispatch-mouse-leave';
export interface ThumbsUpProps {
isThumbsUpSuccessful?: boolean;
onClick?: () => void;
isFrownUponPanelVisible: boolean;
}
export interface ThumbsUpUIProps extends ThumbsUpProps {
isMobile: boolean;
}
export const ThumbsUp: React.FC<ThumbsUpProps> = ({
isThumbsUpSuccessful = false,
onClick,
isFrownUponPanelVisible,
}) => {
const layout = useChatAreaLayout();
const isMobileLayout = layout === Layout.MOBILE;
const reportMessageFeedback = useReportMessageFeedback();
const { message, meta } = useMessageBoxContext();
const latestSectionId = useLatestSectionId();
const handleClick = () => {
reportMessageFeedback({
message_feedback: {
feedback_type: isThumbsUpSuccessful
? MessageFeedbackType.Default
: MessageFeedbackType.Like,
},
}).then(() => {
// 接口调用后再切换展示状态
onClick?.();
});
};
if (!getShowFeedback({ message, meta, latestSectionId })) {
return null;
}
return (
<ThumbsUpUI
isMobile={isMobileLayout}
onClick={handleClick}
isThumbsUpSuccessful={isThumbsUpSuccessful}
isFrownUponPanelVisible={isFrownUponPanelVisible}
/>
);
};
export const ThumbsUpUI: React.FC<ThumbsUpUIProps> = ({
onClick,
isFrownUponPanelVisible,
isMobile,
isThumbsUpSuccessful,
}) => {
const toolTipWrapperRef = useRef<HTMLDivElement>(null);
const isHovering = useHover(toolTipWrapperRef);
useDispatchMouseLeave(toolTipWrapperRef, isFrownUponPanelVisible);
return (
<div ref={toolTipWrapperRef}>
<Tooltip
trigger="custom"
visible={!isMobile && isHovering}
content={I18n.t('like')}
>
<IconButton
data-testid="chat-area.answer-action.thumb-up-button"
size="small"
icon={
isThumbsUpSuccessful ? (
<IconCozThumbsupFill className="w-[14px] h-[14px] coz-fg-color-brand" />
) : (
<IconCozThumbsup className="w-[14px] h-[14px]" />
)
}
color="secondary"
onClick={onClick}
/>
</Tooltip>
</div>
);
};

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
getIsNotificationMessage,
useLatestSectionId,
useMessageBoxContext,
} from '@coze-common/chat-area';
export const useIsFinalAnswerMessageInLastGroup = () => {
const { meta } = useMessageBoxContext();
const latestSectionId = useLatestSectionId();
return (
meta.isGroupLastAnswerMessage &&
meta.isFromLatestGroup &&
meta.sectionId === latestSectionId
);
};
export const useIsNotificationMessage = () => {
const { message } = useMessageBoxContext();
return getIsNotificationMessage(message);
};

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren } from 'react';
import { AnswerActionStoreContext } from '../store/context';
import { AnswerActionPreferenceContext } from '../preference/context';
import { useInitStoreSet } from '../../hooks/use-init-store-set';
export const AnswerActionProvider: React.FC<
PropsWithChildren<{
enableBotTriggerControl: boolean;
}>
> = ({ children, enableBotTriggerControl }) => {
const storeSet = useInitStoreSet();
return (
<AnswerActionStoreContext.Provider value={storeSet}>
<AnswerActionPreferenceContext.Provider
value={{ enableBotTriggerControl }}
>
{children}
</AnswerActionPreferenceContext.Provider>
</AnswerActionStoreContext.Provider>
);
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 PropsWithChildren } from 'react';
import { useRequest } from 'ahooks';
import {
type ReportMessageFeedbackFn,
type ReportMessageFeedbackFnProviderProps,
} from './type';
export const ReportMessageFeedbackFnContext =
createContext<ReportMessageFeedbackFn | null>(null);
export const ReportMessageFeedbackFnProvider: React.FC<
PropsWithChildren<ReportMessageFeedbackFnProviderProps>
> = ({ children, reportMessageFeedback }) => {
const { runAsync: asyncReportMessage } = useRequest(reportMessageFeedback, {
manual: true,
});
return (
<ReportMessageFeedbackFnContext.Provider value={asyncReportMessage}>
{children}
</ReportMessageFeedbackFnContext.Provider>
);
};

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 { useContext } from 'react';
import { ReportMessageFeedbackFnContext } from './context';
export { ReportMessageFeedbackFnProvider } from './context';
export const useReportMessageFeedbackFn = () => {
const reportMessageFeedbackFn = useContext(ReportMessageFeedbackFnContext);
if (!reportMessageFeedbackFn) {
throw new Error('reportMessageFeedbackFn not provided');
}
return reportMessageFeedbackFn;
};

View File

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

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 { createContext } from 'react';
import { type StoreSet } from './type';
export const AnswerActionStoreContext = createContext<StoreSet | null>(null);

View File

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

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 FavoriteBotTriggerConfigStore } from '../../store/favorite-bot-trigger-config';
export interface StoreSet {
useFavoriteBotTriggerConfigStore: FavoriteBotTriggerConfigStore;
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type RefObject, useEffect } from 'react';
/**
* 点击赞、踩按钮,可以关闭打开原因填写面板
* 填写面板关闭的时候, 会造成一次 Reflow。此时赞、踩按钮的位置会发生变化 鼠标已经不在按钮上,但是对应按钮元素不会处罚 mouseleave 事件
* 由于不触发 mouseleave 造成按钮上的 tooltip 不消失、错位等问题
* 所以需要在面板 visible 变化时 patch 一个 mouseleave 事件
*/
export const useDispatchMouseLeave = (
ref: RefObject<HTMLDivElement>,
isFrownUponPanelVisible: boolean,
) => {
useEffect(() => {
ref.current?.dispatchEvent(
new MouseEvent('mouseleave', {
view: window,
bubbles: true,
cancelable: true,
}),
);
}, [isFrownUponPanelVisible, ref.current]);
};

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 { useRequest } from 'ahooks';
import { type GetBotParticipantInfoByBotIdsResponse } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { type BotParticipantInfoWithId } from '../store/favorite-bot-trigger-config';
import { useAnswerActionStore } from '../context/store';
export const useGetBotParticipantInfo = ({
botId,
isEnabled,
}: {
botId: string | undefined;
isEnabled: boolean;
}) => {
const { useFavoriteBotTriggerConfigStore } = useAnswerActionStore();
useRequest(
(): Promise<GetBotParticipantInfoByBotIdsResponse | undefined> => {
if (!botId) {
return Promise.resolve(undefined);
}
return DeveloperApi.GetBotParticipantInfoByBotIds({
bot_ids: [botId],
});
},
{
ready: isEnabled && Boolean(botId),
refreshDeps: [isEnabled, botId],
onSuccess: res => {
if (!res) {
return;
}
const { updateMapByConfigList } =
useFavoriteBotTriggerConfigStore.getState();
const participantInfoList: BotParticipantInfoWithId[] = Object.entries(
res.participant_info_map ?? {},
).map(([participantBotId, item]) => ({
...item,
botId: participantBotId,
}));
updateMapByConfigList(participantInfoList);
},
},
);
};

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 { useCreation } from 'ahooks';
import { createFavoriteBotTriggerConfigStore } from '../store/favorite-bot-trigger-config';
import { type StoreSet } from '../context/store/type';
export const useInitStoreSet = (): StoreSet => {
const useFavoriteBotTriggerConfigStore = useCreation(
() => createFavoriteBotTriggerConfigStore(),
[],
);
return { useFavoriteBotTriggerConfigStore };
};

View File

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

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useToggle } from 'ahooks';
import {
ReportMessageAction,
type ReportMessageProps,
} from '@coze-common/chat-core';
import { getReportError } from '@coze-common/chat-area-utils';
import {
isFallbackErrorMessage,
useChatArea,
useMessageBoxContext,
} from '@coze-common/chat-area';
import { ReportEventNames } from '../report-events';
import { useReportMessageFeedbackFn } from '../context/report-message-feedback';
/**
* @description 消息点赞/点踩
*/
export const useReportMessageFeedback = () => {
const { reporter } = useChatArea();
const asyncReportMessage = useReportMessageFeedbackFn();
const { message } = useMessageBoxContext();
const { message_id } = message;
const reportMessageFeedback = async (
params: Pick<ReportMessageProps, 'message_feedback'>,
) => {
if (isFallbackErrorMessage(message)) {
return;
}
try {
await asyncReportMessage({
message_id,
action: ReportMessageAction.Feedback,
...params,
});
reporter.successEvent({ eventName: ReportEventNames.ReportMessage });
} catch (e) {
reporter.errorEvent({
eventName: ReportEventNames.ReportMessage,
...getReportError(e),
});
}
};
return reportMessageFeedback;
};
/**
* @description 获取 点赞按钮组件/点踩按钮组件/点踩原因填写面板组件 props
*/
export const useReportMessageFeedbackHelpers = () => {
// 点赞成功标识
const [isThumbsUpSuccessful, { toggle: toogleIsThumbsUpSuccessful }] =
useToggle<boolean>(false);
// 点踩成功标识
const [isFrownUponSuccessful, { toggle: toogleIsFrownUponSuccessful }] =
useToggle<boolean>(false);
// 点踩原因填写面板展示
const [
isFrownUponPanelVisible,
{
setLeft: setIsFrownUponPanelVisibleFalse,
setRight: setIsFrownUponPanelVisibleTrue,
},
] = useToggle<boolean>(false);
// 点赞按钮组件onClick事件
const thumbsUpOnClick = () => {
toogleIsThumbsUpSuccessful();
// 点赞/点踩互斥
if (!isThumbsUpSuccessful && isFrownUponSuccessful) {
toogleIsFrownUponSuccessful();
setIsFrownUponPanelVisibleFalse();
}
};
// 点踩按钮组件onClick事件
const frownUponOnClick = () => {
toogleIsFrownUponSuccessful();
// 点赞/点踩互斥
if (!isFrownUponSuccessful && isThumbsUpSuccessful) {
toogleIsThumbsUpSuccessful();
}
if (!isFrownUponSuccessful) {
setIsFrownUponPanelVisibleTrue();
} else {
setIsFrownUponPanelVisibleFalse();
}
};
// 点踩原因填写面板组件onCancel事件
const frownUponPanelonCancel = () => {
setIsFrownUponPanelVisibleFalse();
};
// 点踩原因填写面板组件onSubmit事件
const frownUponPanelonSubmit = () => {
setIsFrownUponPanelVisibleFalse();
// 点赞/点踩互斥
if (isThumbsUpSuccessful) {
toogleIsThumbsUpSuccessful();
}
if (!isFrownUponSuccessful) {
toogleIsFrownUponSuccessful();
}
};
return {
isThumbsUpSuccessful,
isFrownUponSuccessful,
isFrownUponPanelVisible,
thumbsUpOnClick,
frownUponOnClick,
frownUponPanelonCancel,
frownUponPanelonSubmit,
};
};

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 { useChatAreaLayout } from '@coze-common/chat-area/hooks/public/common';
import { Layout } from '@coze-common/chat-uikit-shared';
export const useTooltipTrigger = (
defaultTrigger: 'hover' | 'click' | 'focus' | 'contextMenu',
) => {
const layout = useChatAreaLayout();
if (layout === Layout.MOBILE) {
return 'custom';
}
return defaultTrigger;
};

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useRequest } from 'ahooks';
import { TriggerEnabled } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
export const useUpdateHomeTriggerConfig = ({
botId,
onSuccess,
}: {
botId: string | undefined;
onSuccess?: (isKeepReceiveTrigger: boolean) => void;
}) => {
const { run, loading } = useRequest(
async ({ isKeepReceiveTrigger }: { isKeepReceiveTrigger: boolean }) => {
if (!botId) {
throw new Error('try to request home trigger but no bot id');
}
await DeveloperApi.UpdateHomeTriggerUserConfig({
bot_id: botId,
action: isKeepReceiveTrigger
? TriggerEnabled.Open
: TriggerEnabled.Close,
});
return isKeepReceiveTrigger;
},
{
manual: true,
onSuccess,
},
);
return {
keepReceiveHomeTrigger: () => run({ isKeepReceiveTrigger: true }),
stopReceiveHomeTrigger: () => run({ isKeepReceiveTrigger: false }),
loading,
};
};

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { ActionBarContainer } from './components/action-bar-container';
export { ActionBarHoverContainer } from './components/action-bar-hover-container';
export {
ThumbsUp,
ThumbsUpUI,
ThumbsUpProps,
ThumbsUpUIProps,
} from './components/thumbs-up';
export { RegenerateMessage } from './components/regenerate-message';
export { MoreOperations } from './components/more-operations';
export {
FrownUpon,
FrownUponUI,
FrownUponProps,
FrownUponUIProps,
FrownUponPanel,
FrownUponPanelUI,
FrownUponPanelProps,
FrownUponPanelUIProps,
OnFrownUponSubmitParam,
} from './components/frown-upon';
export { DeleteMessage } from './components/delete-message';
export { CopyTextMessage } from './components/copy-text-message';
export { QuoteMessage } from './components/quote-message';
export {
useReportMessageFeedback,
useReportMessageFeedbackHelpers,
} from './hooks/use-report-message-feedback';
export { useTooltipTrigger } from './hooks/use-tooltip-trigger';
export { AnswerActionProvider } from './context/main';
export {
AnswerActionDivider,
type AnswerActionDividerProps,
} from './components/divider';
export {
BotTriggerConfigButtonGroup,
type BotTriggerConfigButtonGroupProps,
} from './components/bot-trigger-config-button-group';
export { useAnswerActionStore } from './context/store';
export {
ReportMessageFeedbackFnProvider,
useReportMessageFeedbackFn,
} from './context/report-message-feedback';
export { BotParticipantInfoWithId } from './store/favorite-bot-trigger-config';
export { useUpdateHomeTriggerConfig } from './hooks/use-update-home-trigger-config';
export { useDispatchMouseLeave } from './hooks/use-dispatch-mouse-leave';
export { ReportEventNames } from './report-events';

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.
*/
export enum ReportEventNames {
/** 原名: chat_area_tts_voice_ws */
TtsVoiceWs = 'chat_answer_action_start_TTS',
/** 原名: chat_area_report_message */
ReportMessage = 'chat_answer_action_message_feedback',
/** 原名: chat_area_copy_text_message */
CopyTextMessage = 'chat_answer_action_copy_text_message',
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 } from 'zustand/middleware';
import { create } from 'zustand';
import { produce } from 'immer';
import { type BotParticipantInfo } from '@coze-arch/bot-api/developer_api';
type BotId = string;
export type BotParticipantInfoWithId = BotParticipantInfo & { botId: string };
export interface FavoriteBotTriggerConfigState {
favoriteBotTriggerConfigMap: Record<BotId, BotParticipantInfo>;
}
export interface FavoriteBotTriggerConfigAction {
updateFavoriteBotTriggerConfigMap: (
map: Record<BotId, BotParticipantInfo>,
) => void;
updateFavoriteBotTriggerConfigMapByImmer: (
updateFn: (map: Record<BotId, BotParticipantInfo>) => void,
) => void;
updateMapByConfigList: (list: BotParticipantInfoWithId[]) => void;
getFavoriteBotConfigIdList: () => BotId[];
deleteConfigById: (id: BotId) => void;
}
export const createFavoriteBotTriggerConfigStore = () =>
create<FavoriteBotTriggerConfigState & FavoriteBotTriggerConfigAction>()(
devtools(
(set, get) => ({
favoriteBotTriggerConfigMap: {},
updateFavoriteBotTriggerConfigMap: map => {
set(
{
favoriteBotTriggerConfigMap: Object.assign(
{},
get().favoriteBotTriggerConfigMap,
map,
),
},
false,
'updateFavoriteBotTriggerConfigMap',
);
},
updateMapByConfigList: list => {
const map = Object.fromEntries(list.map(item => [item.botId, item]));
set(
{
favoriteBotTriggerConfigMap: Object.assign(
{},
get().favoriteBotTriggerConfigMap,
map,
),
},
false,
'updateMapByConfigList',
);
},
updateFavoriteBotTriggerConfigMapByImmer: updateFn => {
set(
{
favoriteBotTriggerConfigMap: produce<
FavoriteBotTriggerConfigState['favoriteBotTriggerConfigMap']
>(get().favoriteBotTriggerConfigMap, updateFn),
},
false,
'updateFavoriteBotTriggerConfigMapByImmer',
);
},
deleteConfigById: id => {
set(
{
favoriteBotTriggerConfigMap: produce(
get().favoriteBotTriggerConfigMap,
map => {
delete map[id];
},
),
},
false,
'deleteConfigById',
);
},
getFavoriteBotConfigIdList: () =>
Object.entries(get().favoriteBotTriggerConfigMap).map(([id]) => id),
}),
{
enabled: IS_DEV_MODE,
name: 'botStudio.ChatAnswerActionBotTrigger',
},
),
);
export type FavoriteBotTriggerConfigStore = ReturnType<
typeof createFavoriteBotTriggerConfigStore
>;

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,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 { type MessageMeta } from '@coze-common/chat-area';
export const getIsLastGroup = ({
meta: { isFromLatestGroup, sectionId },
latestSectionId,
}: {
meta: Pick<MessageMeta, 'isFromLatestGroup' | 'sectionId'>;
latestSectionId: string;
}) => isFromLatestGroup && sectionId === latestSectionId;

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 Message,
getIsNotificationMessage,
getIsTriggerMessage,
getIsAsyncResultMessage,
} from '@coze-common/chat-area';
export const getIsPushedMessage = (
message: Pick<Message, 'type' | 'source'>,
): boolean =>
getIsTriggerMessage(message) ||
getIsNotificationMessage(message) ||
getIsAsyncResultMessage(message);

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 { messageSource, taskType } from '@coze-common/chat-core';
import { type Message, type MessageMeta } from '@coze-common/chat-area';
import { getIsLastGroup } from './get-is-last-group';
export const getShowBotTriggerButton = ({
// eslint-disable-next-line @typescript-eslint/naming-convention -- .
message: { source, extra_info },
meta,
latestSectionId,
}: {
message: Pick<Message, 'source' | 'extra_info'>;
meta: Pick<MessageMeta, 'isFromLatestGroup' | 'sectionId'>;
latestSectionId: string;
}) =>
source === messageSource.TaskManualTrigger &&
extra_info.task_type === taskType.PresetTask &&
getIsLastGroup({ meta, latestSectionId });

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 { type Message, type MessageMeta } from '@coze-common/chat-area';
import { getIsPushedMessage } from './get-is-pushed-message';
export const getShowFeedback = ({
message,
meta,
latestSectionId,
}: {
message: Pick<Message, 'type' | 'source'>;
meta: Pick<
MessageMeta,
'isFromLatestGroup' | 'sectionId' | 'isGroupLastAnswerMessage'
>;
latestSectionId: string;
}): boolean => {
// 是否是推送的消息
const isPushedMessage = getIsPushedMessage(message);
if (isPushedMessage) {
return false;
}
// 来自最后一个消息组的 final answer
return (
meta.isGroupLastAnswerMessage &&
meta.isFromLatestGroup &&
meta.sectionId === latestSectionId
);
};

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 Message, type MessageMeta } from '@coze-common/chat-area';
import { getIsPushedMessage } from './get-is-pushed-message';
import { getIsLastGroup } from './get-is-last-group';
export const getShowRegenerate = ({
message,
meta,
latestSectionId,
}: {
message: Pick<Message, 'type' | 'source'>;
meta: Pick<MessageMeta, 'isFromLatestGroup' | 'sectionId'>;
latestSectionId: string;
}): boolean => {
// 是否是推送的消息
const isPushedMessage = getIsPushedMessage(message);
if (isPushedMessage) {
return false;
}
// 来自最后一个消息组
return getIsLastGroup({ meta, latestSectionId });
};

View File

@@ -0,0 +1,70 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"types": [],
"strictNullChecks": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true,
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../../arch/bot-flags/tsconfig.build.json"
},
{
"path": "../../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../../arch/bot-utils/tsconfig.build.json"
},
{
"path": "../../../arch/i18n/tsconfig.build.json"
},
{
"path": "../chat-area/tsconfig.build.json"
},
{
"path": "../chat-core/tsconfig.build.json"
},
{
"path": "../chat-uikit-shared/tsconfig.build.json"
},
{
"path": "../chat-uikit/tsconfig.build.json"
},
{
"path": "../../../components/bot-icons/tsconfig.build.json"
},
{
"path": "../../../components/bot-semi/tsconfig.build.json"
},
{
"path": "../../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../plugin-message-grab/tsconfig.build.json"
},
{
"path": "../text-grab/tsconfig.build.json"
},
{
"path": "../utils/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,19 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"],
"strictNullChecks": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true
}
}

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 { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
test: {
coverage: {
all: true,
exclude: ['index.ts'],
},
},
});