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,3 @@
.interrupt-message-box {
margin-top: 8px;
}

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 { useState } from 'react';
import classNames from 'classnames';
import { MessageBox as UIKitMessageBox } from '@coze-common/chat-uikit';
import { type CustomComponent } from '@coze-common/chat-area';
import { InterruptMessageContent } from './interrupt-message-content';
import styles from './index.module.less';
export const InterruptMessageBox: CustomComponent['MessageBox'] = props => {
// 用户操作后文案,前端维护暂时状态,刷新消失
const [actionText, setActionText] = useState('');
const { message, meta } = props;
// 不展示逻辑: 为历史消息、无action且不在最后一个group
if (message._fromHistory || (!actionText && !meta.isFromLatestGroup)) {
return null;
}
return (
<div className={classNames(styles['interrupt-message-box'])}>
<UIKitMessageBox
{...props}
messageId={message.message_id}
senderInfo={{ id: '' }}
showUserInfo={false}
theme={actionText ? 'none' : 'border'}
>
<InterruptMessageContent
interruptMessage={message}
actionText={actionText}
setActionText={setActionText}
/>
</UIKitMessageBox>
</div>
);
};
InterruptMessageBox.displayName = 'ChatAreaFunctionCallMessageBox';

View File

@@ -0,0 +1,34 @@
.interrupt-message-box {
user-select: none;
overflow: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
width: 100%;
font-weight: 400;
line-height: 16px;
word-break: break-word;
// 用户点击授权后文案样式,无背景
.interrupt-message-action {
padding: 6px;
color: var(--fg-coze-fg-secondary, rgba(6, 7, 9, 50%));
}
// 中断消息内容
.interrupt-message-content {
padding: 12px;
color: var(--fg-coze-fg-primary, rgba(6, 7, 9, 80%));
background-color: var(--bg-coze-bg-max, #FFF);
.interrupt-message-content-btns {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 {
PluginName,
useWriteablePlugin,
type Message,
} from '@coze-common/chat-area';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { Space } from '@coze-arch/bot-semi';
import { useGetPosition } from '../../../hooks/use-get-position';
import styles from './index.module.less';
export const InterruptMessageContent: React.FC<{
interruptMessage: Message;
actionText?: string;
setActionText: (actionText: string) => void;
}> = ({ interruptMessage, actionText, setActionText }) => {
const plugin = useWriteablePlugin<unknown>(PluginName.Resume);
const { sendResumeMessage, stopResponding } =
plugin.chatAreaPluginContext?.writeableAPI.message ?? {};
// 获取中断场景、续聊id
const toolCall =
interruptMessage.required_action?.submit_tool_outputs?.tool_calls?.[0];
const { loading, getSysPosition: handleAllowOnce } = useGetPosition({
getPositionSuccess: position => {
setActionText(
I18n.t('chat_geolocation_auth_allow_tip', {
plugin: toolCall?.require_info?.name ?? 'plugin',
}),
);
sendResumeMessage?.({
replyId: interruptMessage.reply_id,
options: {
extendFiled: {
interrupt_message_id: interruptMessage.message_id,
resume_message_id: interruptMessage.reply_id,
tool_outputs: [
{
tool_call_id: toolCall?.id,
output: JSON.stringify({
coordinates: {
longitude: String(position.coords.longitude),
latitude: String(position.coords.latitude),
},
}),
},
],
},
},
});
},
});
const handleReject = () => {
setActionText(
I18n.t('chat_geolocation_auth_decline_tip', {
plugin: toolCall?.require_info?.name ?? 'plugin',
}),
);
stopResponding?.();
};
return (
<div className={classNames(styles['interrupt-message-box'])}>
{actionText ? (
<div className={classNames(styles['interrupt-message-action'])}>
{actionText}
</div>
) : (
<div className={classNames(styles['interrupt-message-content'])}>
{toolCall?.require_info?.require_fields?.includes('coordinates') ? (
<>
{I18n.t('chat_geolocation_auth_request_message', {
plugin_name: toolCall?.require_info?.name ?? 'plugin',
})}
<Space
className={classNames(styles['interrupt-message-content-btns'])}
>
<Button
color="highlight"
size="small"
loading={loading}
onClick={handleAllowOnce}
>
{I18n.t('chat_geolocation_auth_request_message_allow_button')}
</Button>
<Button
color="primary"
size="small"
disabled={loading}
onClick={handleReject}
>
{I18n.t(
'chat_geolocation_auth_request_message_decline_button',
)}
</Button>
</Space>
</>
) : null}
</div>
)}
</div>
);
};
InterruptMessageContent.displayName = 'ChatAreaFunctionCallMessageContent';