137 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|