Files
Genarrative/src/components/image-editor/EditorAgentConversation/MessageBubble.tsx
T

137 lines
4.2 KiB
TypeScript

import {
EDITOR_AGENT_ERROR_MESSAGE_PREFIX,
type EditorAgentMessage,
} from '@/packages/shared/src/contracts';
import AttachmentChip from '@/src/components/image-editor/EditorAgentConversation/AttachmentChip.tsx';
import { attachmentKey } from '@/src/components/image-editor/EditorAgentConversation/common.ts';
import { PendingToolCall } from '@/src/components/image-editor/EditorAgentConversation/PendingToolCall.tsx';
import ToolCallView from '@/src/components/image-editor/EditorAgentConversation/ToolCallView.tsx';
function messageRoleLabel(role: EditorAgentMessage['role']) {
if (role === 'user') {
return '你';
}
return 'Agent';
}
export function ThinkingBubble() {
return (
<article className="flex justify-start" aria-label="Agent思考中">
<div className="max-w-[86%] rounded-3xl border border-slate-200 bg-white px-3.5 py-3 text-sm leading-6 shadow-sm">
<div className="flex items-center gap-1.5">
<span className="flex gap-0.5">
<span
className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400"
style={{ animationDelay: '0ms' }}
/>
<span
className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400"
style={{ animationDelay: '150ms' }}
/>
<span
className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400"
style={{ animationDelay: '300ms' }}
/>
</span>
</div>
</div>
</article>
);
}
type MessageBubbleProps = {
message: EditorAgentMessage;
busyAction: 'confirm' | 'cancel' | null;
onConfirmToolCall: (messageId: number) => Promise<void>;
onCancelToolCall: (messageId: number) => Promise<void>;
onJobCompleted?: () => void;
};
export function MessageBubble({
message,
busyAction,
onConfirmToolCall,
onCancelToolCall,
onJobCompleted,
}: MessageBubbleProps) {
const systemErrorText =
message.role === 'system' &&
!message.toolCall &&
message.text.startsWith(EDITOR_AGENT_ERROR_MESSAGE_PREFIX)
? message.text.slice(EDITOR_AGENT_ERROR_MESSAGE_PREFIX.length)
: null;
if (message.role === 'system' && !message.toolCall && systemErrorText === null) {
return null;
}
if (
message.role === 'system' &&
message.toolCall &&
message.toolCall.status === 'not_completed' &&
!message.toolCall.externalJobId
) {
return (
<PendingToolCall
messageId={message.id}
toolCall={message.toolCall}
busyAction={busyAction}
onConfirm={onConfirmToolCall}
onCancel={onCancelToolCall}
/>
);
}
const isUser = message.role === 'user';
const isSystem = message.role === 'system';
const isSystemError = systemErrorText !== null;
return (
<article
className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}
aria-label={
isSystemError
? 'Agent错误'
: isSystem
? 'Agent操作'
: `${messageRoleLabel(message.role)}消息`
}
>
<div
className={
isSystem && !isSystemError
? 'max-w-[86%]'
: `max-w-[86%] rounded-3xl px-3.5 py-3 text-sm leading-6 shadow-sm ${
isUser
? 'bg-slate-900 text-white'
: isSystemError || message.toolCall?.status === 'failed'
? 'border border-red-200 bg-red-50 text-red-700'
: 'border border-slate-200 bg-white text-slate-700'
}`
}
>
{(!isSystem || isSystemError) && (systemErrorText ?? message.text) ? (
<div className="whitespace-pre-wrap break-words">
{systemErrorText ?? message.text}
</div>
) : null}
{!isSystem && message.attachments.length ? (
<div className="mt-2 flex flex-wrap gap-1.5">
{message.attachments.map((attachment) => (
<AttachmentChip
key={attachmentKey(attachment)}
attachment={attachment}
/>
))}
</div>
) : null}
{message.toolCall ? (
<ToolCallView
toolCall={message.toolCall}
onJobCompleted={onJobCompleted}
/>
) : null}
</div>
</article>
);
}