💄 style: fix approving render and improve Conversation style (#10210)

fix approving render and improve chat layout style
This commit is contained in:
Arvin Xu
2025-11-14 12:57:28 +08:00
committed by GitHub
parent afd3a47e3d
commit 841b7f1c37
9 changed files with 131 additions and 44 deletions
@@ -1,3 +1,4 @@
import { useTheme } from 'antd-style';
import { Suspense, memo } from 'react';
import { Flexbox } from 'react-layout-kit';
@@ -18,30 +19,38 @@ interface WorkspaceLayoutProps {
mobile?: boolean;
}
const DesktopWorkspace = memo(() => (
<>
<ChatHeaderDesktop />
<Flexbox
height={'100%'}
horizontal
style={{ overflow: 'hidden', position: 'relative' }}
width={'100%'}
>
<Flexbox height={'100%'} style={{ overflow: 'hidden', position: 'relative' }} width={'100%'}>
<ConversationArea mobile={false} />
const DesktopWorkspace = memo(() => {
const theme = useTheme();
return (
<>
<ChatHeaderDesktop />
<Flexbox
height={'100%'}
horizontal
style={{ overflow: 'hidden', position: 'relative' }}
width={'100%'}
>
<Flexbox
height={'100%'}
style={{ background: theme.colorBgContainer, overflow: 'hidden', position: 'relative' }}
width={'100%'}
>
<ConversationArea mobile={false} />
</Flexbox>
<Portal>
<Suspense fallback={<BrandTextLoading />}>
<PortalPanel mobile={false} />
</Suspense>
</Portal>
<TopicPanel>
<TopicSidebar mobile={false} />
</TopicPanel>
</Flexbox>
<Portal>
<Suspense fallback={<BrandTextLoading />}>
<PortalPanel mobile={false} />
</Suspense>
</Portal>
<TopicPanel>
<TopicSidebar mobile={false} />
</TopicPanel>
</Flexbox>
<MainInterfaceTracker />
</>
));
<MainInterfaceTracker />
</>
);
});
DesktopWorkspace.displayName = 'DesktopWorkspace';
@@ -13,6 +13,7 @@ import { useStyles } from '../style';
import { ChatItemProps } from '../type';
export interface MessageContentProps {
className?: string;
disabled?: ChatItemProps['disabled'];
editing?: ChatItemProps['editing'];
id: string;
@@ -39,6 +40,7 @@ const MessageContent = memo<MessageContentProps>(
onDoubleClick,
markdownProps,
disabled,
className,
}) => {
const { t } = useTranslation('common');
const { cx, styles } = useStyles({ disabled, editing, placement, primary, variant });
@@ -81,7 +83,7 @@ const MessageContent = memo<MessageContentProps>(
return (
<Flexbox
className={cx(styles.message, editing && styles.editingContainer)}
className={cx(styles.message, editing && styles.editingContainer, className)}
onDoubleClick={onDoubleClick}
>
{messageContent}
@@ -28,10 +28,12 @@ const ApprovalActions = memo<ApprovalActionsProps>(
const [approveLoading, setApproveLoading] = useState(false);
const { assistantGroupId } = useGroupMessage();
const [approveToolIntervention, rejectToolIntervention] = useChatStore((s) => [
s.approveToolCalling,
s.rejectToolCalling,
]);
const [approveToolIntervention, rejectToolIntervention, rejectAndContinueToolIntervention] =
useChatStore((s) => [
s.approveToolCalling,
s.rejectToolCalling,
s.rejectAndContinueToolCalling,
]);
const addToolToAllowList = useUserStore((s) => s.addToolToAllowList);
const handleApprove = async (remember?: boolean) => {
@@ -58,6 +60,14 @@ const ApprovalActions = memo<ApprovalActionsProps>(
setRejectReason('');
};
const handleRejectAndContinue = async (reason?: string) => {
setRejectLoading(true);
await rejectAndContinueToolIntervention(messageId, reason);
setRejectLoading(false);
setRejectPopoverOpen(false);
setRejectReason('');
};
return (
<Flexbox gap={8} horizontal>
<Popover
@@ -67,14 +77,25 @@ const ApprovalActions = memo<ApprovalActionsProps>(
<Flexbox align={'center'} horizontal justify={'space-between'}>
<div>{t('tool.intervention.rejectTitle')}</div>
<Button
loading={rejectLoading}
onClick={() => handleReject(rejectReason)}
size="small"
type="primary"
>
{t('confirm', { ns: 'common' })}
</Button>
<Space>
<Button
color={'default'}
loading={rejectLoading}
onClick={() => handleReject(rejectReason)}
size="small"
variant={'filled'}
>
{t('tool.intervention.rejectOnly')}
</Button>
<Button
loading={rejectLoading}
onClick={() => handleRejectAndContinue(rejectReason)}
size="small"
type="primary"
>
{t('tool.intervention.rejectAndContinue')}
</Button>
</Space>
</Flexbox>
<Input.TextArea
autoFocus
@@ -95,7 +116,7 @@ const ApprovalActions = memo<ApprovalActionsProps>(
placement="bottomRight"
trigger="click"
>
<Button size="small" type="default">
<Button color={'default'} size="small" variant={'filled'}>
{t('tool.intervention.reject')}
</Button>
</Popover>
@@ -1,6 +1,6 @@
import { UIChatMessage } from '@lobechat/types';
import { Tag } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import { createStyles, useResponsive } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { ReactNode, memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -45,6 +45,13 @@ const remarkPlugins = markdownElements
.map((element) => element.remarkPlugin)
.filter(Boolean);
const useUserStyles = createStyles(({ css, token }) => ({
messageContainer: css`
border: none;
background: ${token.colorFillTertiary};
`,
}));
const UserMessage = memo<UserMessageProps>(({ id, disableEditing, index }) => {
const item = useChatStore(
displayMessageSelectors.getDisplayMessageById(id),
@@ -56,6 +63,8 @@ const UserMessage = memo<UserMessageProps>(({ id, disableEditing, index }) => {
const { t } = useTranslation('chat');
const { mobile } = useResponsive();
const avatar = useUserAvatar();
const { styles: userStyles } = useUserStyles();
const title = useUserStore(userProfileSelectors.displayUserName);
const displayMode = useAgentStore(agentChatConfigSelectors.displayMode);
@@ -165,6 +174,7 @@ const UserMessage = memo<UserMessageProps>(({ id, disableEditing, index }) => {
>
<Flexbox flex={1} style={{ maxWidth: '100%', minWidth: 0 }}>
<MessageContent
className={userStyles.messageContainer}
editing={editing}
id={id}
markdownProps={markdownProps}
+2
View File
@@ -414,6 +414,8 @@ export default {
manualDesc: '每次调用都需要手动批准',
},
reject: '拒绝',
rejectAndContinue: '拒绝后重试执行',
rejectOnly: '拒绝',
rejectReasonPlaceholder: '输入拒绝原因将帮助 Agent 理解并优化后续行动',
rejectTitle: '拒绝本次工具调用',
rejectedWithReason: '本次工具调用被主动拒绝:{{reason}}',
@@ -43,6 +43,10 @@ export interface ConversationControlAction {
* Reject tool intervention
*/
rejectToolCalling: (messageId: string, reason?: string) => Promise<void>;
/**
* Reject tool intervention and continue
*/
rejectAndContinueToolCalling: (messageId: string, reason?: string) => Promise<void>;
/**
* Toggle sendMessage operation state
*/
@@ -206,6 +210,44 @@ export const conversationControl: StateCreator<
await get().optimisticUpdateMessageContent(messageId, toolContent);
},
rejectAndContinueToolCalling: async (messageId, reason) => {
await get().rejectToolCalling(messageId, reason);
const toolMessage = dbMessageSelectors.getDbMessageById(messageId)(get());
if (!toolMessage) return;
// Get current messages for state construction
const currentMessages = displayMessageSelectors.mainAIChats(get());
const { activeThreadId, internal_execAgentRuntime } = get();
// Create agent state and context to continue from rejected tool message
const { state, context: initialContext } = get().internal_createAgentState({
messages: currentMessages,
parentMessageId: messageId,
threadId: activeThreadId,
});
// Override context with 'userInput' phase to continue as if user provided feedback
const context: AgentRuntimeContext = {
...initialContext,
phase: 'user_input',
};
// Execute agent runtime from rejected tool message position to continue
try {
await internal_execAgentRuntime({
messages: currentMessages,
parentMessageId: messageId,
parentMessageType: 'tool',
threadId: activeThreadId,
initialState: state,
initialContext: context,
});
} catch (error) {
console.error('[rejectAndContinueToolCalling] Error executing agent runtime:', error);
}
},
internal_updateSendMessageOperation: (key, value, actionName) => {
const operationKey = typeof key === 'string' ? key : messageMapKey(key.sessionId, key.topicId);
@@ -3,6 +3,8 @@ import { Highlighter, Text } from '@lobehub/ui';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { BuiltinInterventionProps } from '@/types/tool';
const formatTimeout = (ms?: number) => {
if (!ms) return null;
@@ -23,11 +25,8 @@ const formatTimeout = (ms?: number) => {
return `${ms}ms`;
};
interface RunCommandProps extends RunCommandParams {
messageId: string;
}
const RunCommand = memo<RunCommandProps>(({ description, command, timeout }) => {
const RunCommand = memo<BuiltinInterventionProps<RunCommandParams>>(({ args }) => {
const { description, command, timeout } = args;
return (
<Flexbox gap={8}>
<Flexbox horizontal justify={'space-between'}>
@@ -26,9 +26,10 @@ const useStyles = createStyles(({ css, token, cx }) => ({
height: 64px;
padding: 8px;
border: 1px solid ${token.colorBorderSecondary};
border-radius: ${token.borderRadiusLG}px;
background: ${token.colorFillQuaternary};
transition: all 0.2s ${token.motionEaseInOut};
.local-file-actions {
+1
View File
@@ -186,6 +186,7 @@ export const LocalSystemManifest: BuiltinToolManifest = {
{
description:
'Write content to a specific file. Input should be the file path and content. Overwrites existing file or creates a new one.',
humanIntervention: 'required',
name: LocalSystemApiName.writeLocalFile,
parameters: {
properties: {