9bb4942ae7
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/95 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
193 lines
6.0 KiB
TypeScript
193 lines
6.0 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,
|
|
type EditorAgentContextAsset,
|
|
} 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';
|
|
|
|
import { MessageBubbleRightClickMenu } from './MessageBubbleRightClickMenu.tsx';
|
|
import { useRightClickMenu } from './useRightClickMenu.ts';
|
|
|
|
function messageRoleLabel(role: EditorAgentMessage['role']) {
|
|
if (role === 'user') {
|
|
return '你';
|
|
}
|
|
return 'Agent';
|
|
}
|
|
|
|
export function ThinkingBubble({
|
|
showPatienceNotice = false,
|
|
}: {
|
|
showPatienceNotice?: boolean;
|
|
}) {
|
|
return (
|
|
<article
|
|
className="flex justify-start"
|
|
aria-label={showPatienceNotice ? 'Agent仍在处理中' : '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>
|
|
{showPatienceNotice ? (
|
|
<span className="text-slate-600">仍在处理中,请耐心等待</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
type MessageBubbleProps = {
|
|
message: EditorAgentMessage;
|
|
busyAction: 'confirm' | 'cancel' | null;
|
|
onConfirmToolCall: (messageId: number) => Promise<void>;
|
|
onCancelToolCall: (messageId: number) => Promise<void>;
|
|
onJobCompleted?: () => void;
|
|
// TODO prop drilling
|
|
onReferenceImage?: (asset: EditorAgentContextAsset) => boolean;
|
|
};
|
|
|
|
export function MessageBubble({
|
|
message,
|
|
busyAction,
|
|
onConfirmToolCall,
|
|
onCancelToolCall,
|
|
onJobCompleted,
|
|
onReferenceImage,
|
|
}: MessageBubbleProps) {
|
|
const {
|
|
rightClickMenu,
|
|
openRightClickMenu,
|
|
closeRightClickMenu,
|
|
runRightClickAction,
|
|
} = useRightClickMenu({ onReferenceImage });
|
|
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;
|
|
|
|
const isUser = message.role === 'user';
|
|
const isSystem = message.role === 'system';
|
|
const isSystemError = systemErrorText !== null;
|
|
const visibleText = systemErrorText ?? (!isSystem ? message.text : '');
|
|
|
|
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}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<article
|
|
className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}
|
|
aria-label={
|
|
isSystemError
|
|
? 'Agent错误'
|
|
: isSystem
|
|
? 'Agent操作'
|
|
: `${messageRoleLabel(message.role)}消息`
|
|
}
|
|
onContextMenu={
|
|
visibleText.trim()
|
|
? (event) =>
|
|
openRightClickMenu(event, { kind: 'text', text: visibleText })
|
|
: undefined
|
|
}
|
|
>
|
|
<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}
|
|
onRightClickMenu={(event, asset) =>
|
|
openRightClickMenu(event, { kind: 'asset', asset })
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
{message.toolCall ? (
|
|
<ToolCallView
|
|
toolCall={message.toolCall}
|
|
onJobCompleted={onJobCompleted}
|
|
onRightClickMenu={(event, asset) =>
|
|
openRightClickMenu(event, { kind: 'asset', asset })
|
|
}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</article>
|
|
{rightClickMenu ? (
|
|
<MessageBubbleRightClickMenu
|
|
x={rightClickMenu.x}
|
|
y={rightClickMenu.y}
|
|
target={rightClickMenu.target}
|
|
pendingAction={rightClickMenu.pendingAction}
|
|
resultAction={rightClickMenu.resultAction}
|
|
result={rightClickMenu.result}
|
|
onAction={(action) => void runRightClickAction(action)}
|
|
onClose={closeRightClickMenu}
|
|
/>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|