fix: 修复聊天窗口布局 - AGENT头像在左侧,用户头像在右侧

main
sp mac bookpro 2605 2026-06-08 00:58:55 +08:00
parent 345a055d1a
commit 32cb1c9d92
2 changed files with 171 additions and 133 deletions

View File

@ -70,71 +70,83 @@ export default function ChatBody(props: {
{streaming.active && ( {streaming.active && (
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 24 }}>
{(() => { <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
const streamingAgentId = streaming.targetAgentId || currentAgentId; {(() => {
const streamingAgent = agentList.find(a => a.id === streamingAgentId); const streamingAgentId = streaming.targetAgentId || currentAgentId;
if (streamingAgent) { const streamingAgent = agentList.find(a => a.id === streamingAgentId);
return ( if (streamingAgent) {
<div className="message-agent-header" style={{ return (
display: 'flex', <Avatar src={streamingAgent.avatar} size={36} style={{ flexShrink: 0, marginTop: 2 }} />
alignItems: 'center', );
gap: 8, }
marginBottom: 6, return null;
marginLeft: 2 })()}
}}> <div style={{ flex: 1, minWidth: 0 }}>
<Avatar src={streamingAgent.avatar} size={24} /> {(() => {
<span style={{ const streamingAgentId = streaming.targetAgentId || currentAgentId;
fontSize: 13, const streamingAgent = agentList.find(a => a.id === streamingAgentId);
fontWeight: 500, if (streamingAgent) {
color: 'var(--color-text)' return (
}}> <div style={{
{streamingAgent.name} display: 'flex',
</span> alignItems: 'center',
</div> gap: 8,
); marginBottom: 6
} }}>
return null; <span style={{
})()} fontSize: 13,
<div className="bubble assistant"> fontWeight: 500,
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> color: 'var(--color-text)'
{!!streaming.retryInfo?.message && ( }}>
<div style={{ padding: '8px 10px', borderRadius: 10, background: 'rgba(59, 130, 246, 0.08)', border: '1px solid rgba(59, 130, 246, 0.18)' }}> {streamingAgent.name}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}> </span>
<span style={{ fontSize: 12.5, color: 'var(--color-text)', fontWeight: 600 }}>{streaming.retryInfo.stage === 'fallback_model' ? '自动切换模型' : '自动重试'}</span> </div>
{streaming.retryInfo.stage === 'fallback_model' ? ( );
<Tag color="processing" style={{ marginInlineEnd: 0 }}> }
{String(streaming.retryInfo.fromModel || '')} {String(streaming.retryInfo.toModel || '')} return null;
</Tag> })()}
) : ( <div className="bubble assistant">
<Tag color="processing" style={{ marginInlineEnd: 0 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{String(streaming.retryInfo.model || '')} {!!streaming.retryInfo?.message && (
{streaming.retryInfo.attempt ? ` · 第${streaming.retryInfo.attempt}` : ''} <div style={{ padding: '8px 10px', borderRadius: 10, background: 'rgba(59, 130, 246, 0.08)', border: '1px solid rgba(59, 130, 246, 0.18)' }}>
</Tag> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
)} <span style={{ fontSize: 12.5, color: 'var(--color-text)', fontWeight: 600 }}>{streaming.retryInfo.stage === 'fallback_model' ? '自动切换模型' : '自动重试'}</span>
</div> {streaming.retryInfo.stage === 'fallback_model' ? (
<div style={{ marginTop: 4, fontSize: 12.5, color: 'var(--color-text-secondary)', lineHeight: 1.55 }}>{String(streaming.retryInfo.message)}</div> <Tag color="processing" style={{ marginInlineEnd: 0 }}>
{!!streaming.retryInfo.reason && ( {String(streaming.retryInfo.fromModel || '')} {String(streaming.retryInfo.toModel || '')}
<div style={{ marginTop: 4, fontSize: 12, color: 'var(--color-text-tertiary)', lineHeight: 1.5 }}>{String(streaming.retryInfo.reason)}</div> </Tag>
) : (
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
{String(streaming.retryInfo.model || '')}
{streaming.retryInfo.attempt ? ` · 第${streaming.retryInfo.attempt}` : ''}
</Tag>
)}
</div>
<div style={{ marginTop: 4, fontSize: 12.5, color: 'var(--color-text-secondary)', lineHeight: 1.55 }}>{String(streaming.retryInfo.message)}</div>
{!!streaming.retryInfo.reason && (
<div style={{ marginTop: 4, fontSize: 12, color: 'var(--color-text-tertiary)', lineHeight: 1.5 }}>{String(streaming.retryInfo.reason)}</div>
)}
</div>
)} )}
<div>
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginBottom: 4 }}></div>
{streaming.reasoningText ? <Markdown>{streaming.reasoningText + '▍'}</Markdown> : <span style={{ color: 'var(--color-text-tertiary)' }}></span>}
</div>
<Divider style={{ margin: '6px 0' }} />
<div>
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginBottom: 4 }}></div>
{streaming.answerText ? <Markdown>{streaming.answerText + '▍'}</Markdown> : <span style={{ color: 'var(--color-text-tertiary)' }}></span>}
</div>
</div>
</div>
{(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && (
<div>
{streaming.retrieved.length > 0 && <RetrievedView retrieved={streaming.retrieved} />}
{streaming.toolCalls.length > 0 && <ToolCallView calls={streaming.toolCalls} liveStyle />}
</div> </div>
)} )}
<div>
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginBottom: 4 }}></div>
{streaming.reasoningText ? <Markdown>{streaming.reasoningText + '▍'}</Markdown> : <span style={{ color: 'var(--color-text-tertiary)' }}></span>}
</div>
<Divider style={{ margin: '6px 0' }} />
<div>
<div style={{ fontSize: 12, color: 'var(--color-text-tertiary)', marginBottom: 4 }}></div>
{streaming.answerText ? <Markdown>{streaming.answerText + '▍'}</Markdown> : <span style={{ color: 'var(--color-text-tertiary)' }}></span>}
</div>
</div> </div>
</div> </div>
{(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && (
<div style={{ maxWidth: '85%' }}>
{streaming.retrieved.length > 0 && <RetrievedView retrieved={streaming.retrieved} />}
{streaming.toolCalls.length > 0 && <ToolCallView calls={streaming.toolCalls} liveStyle />}
</div>
)}
</div> </div>
)} )}
</> </>

View File

@ -43,87 +43,113 @@ export default function MessageItem(props: {
<div <div
id={'msg-' + message.id} id={'msg-' + message.id}
style={{ style={{
marginBottom: 12, marginBottom: 20,
padding: highlighted ? 8 : 0, padding: highlighted ? 8 : 0,
borderRadius: highlighted ? 10 : 0, borderRadius: highlighted ? 10 : 0,
background: highlighted ? 'rgba(254, 243, 199, 0.6)' : 'transparent', background: highlighted ? 'rgba(254, 243, 199, 0.6)' : 'transparent',
transition: 'background 0.4s, padding 0.4s' transition: 'background 0.4s, padding 0.4s'
}} }}
> >
{hasAnswerAgent && ( {message.role === 'assistant' ? (
<div className="message-agent-header" style={{ <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
display: 'flex', {/* AGENT: 头像在左侧 */}
alignItems: 'center', {hasAnswerAgent && (
gap: 8, <Avatar src={answerAgent!.avatar} size={36} style={{ flexShrink: 0, marginTop: 2 }} />
marginBottom: 6,
marginLeft: 2
}}>
<Avatar src={answerAgent!.avatar} size={24} />
<span style={{
fontSize: 13,
fontWeight: 500,
color: 'var(--color-text)'
}}>
{answerAgent!.name}
</span>
</div>
)}
<div className={`bubble ${message.role}`}>
{message.role === 'assistant' ? (
<Markdown>{message.content}</Markdown>
) : message.content.includes('![image](') ? (
<Markdown>{message.content}</Markdown>
) : (
<span dangerouslySetInnerHTML={{
__html: message.content.replace(/@([^\s]+)/g, '<span class="mention">@$1</span>')
}} />
)}
</div>
{message.role === 'assistant' && (
<div className="monica-msg-actions" style={{ maxWidth: '78%' }}>
{hasBranches && (
<Space size={2}>
<Button size="small" type="text" disabled={activeIdx === 0} onClick={goPrev}>
</Button>
<span>
{activeIdx + 1} / {total}
</span>
<Button size="small" type="text" disabled={activeIdx === total - 1} onClick={goNext}>
</Button>
</Space>
)} )}
{message.meta?.aborted && <Tag color="orange"></Tag>} <div style={{ flex: 1, minWidth: 0 }}>
<Dropdown {hasAnswerAgent && (
trigger={['click']} <div style={{
menu={{ display: 'flex',
items: [ alignItems: 'center',
{ key: 'plain', label: '复制纯文本', onClick: () => onCopy?.(message.content, 'plain') }, gap: 8,
{ key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(message.content, 'markdown') } marginBottom: 6
] }}>
}} <span style={{
> fontSize: 13,
<Tooltip title="复制"> fontWeight: 500,
<span className="chat-copy-icon" aria-label="copy"> color: 'var(--color-text)'
<ProductCopyIcon /> }}>
</span> {answerAgent!.name}
</Tooltip> </span>
</Dropdown> </div>
<Tooltip title="重新生成(开新分支)"> )}
<Button size="small" type="text" disabled={busy} onClick={() => onRegenerate?.(message.id)}> <div className={`bubble ${message.role}`}>
🔄 <Markdown>{message.content}</Markdown>
</Button> </div>
</Tooltip> <div className="monica-msg-actions">
{hasBranches && (
<Space size={2}>
<Button size="small" type="text" disabled={activeIdx === 0} onClick={goPrev}>
</Button>
<span>
{activeIdx + 1} / {total}
</span>
<Button size="small" type="text" disabled={activeIdx === total - 1} onClick={goNext}>
</Button>
</Space>
)}
{message.meta?.aborted && <Tag color="orange"></Tag>}
<Dropdown
trigger={['click']}
menu={{
items: [
{ key: 'plain', label: '复制纯文本', onClick: () => onCopy?.(message.content, 'plain') },
{ key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(message.content, 'markdown') }
]
}}
>
<Tooltip title="复制">
<span className="chat-copy-icon" aria-label="copy">
<ProductCopyIcon />
</span>
</Tooltip>
</Dropdown>
<Tooltip title="重新生成(开新分支)">
<Button size="small" type="text" disabled={busy} onClick={() => onRegenerate?.(message.id)}>
🔄
</Button>
</Tooltip>
</div>
{message.meta && (
<div>
{!!message.meta.reasoning && <ReasoningView reasoning={message.meta.reasoning} />}
{!!message.meta.retrieved?.length && <RetrievedView retrieved={message.meta.retrieved} />}
{!!message.meta.toolCalls?.length && <ToolCallView calls={message.meta.toolCalls} />}
</div>
)}
</div>
</div> </div>
)} ) : (
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start', justifyContent: 'flex-end' }}>
{message.role === 'assistant' && message.meta && ( <div style={{ flex: 1, minWidth: 0, maxWidth: '78%', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{ maxWidth: '78%' }}> <div style={{
{!!message.meta.reasoning && <ReasoningView reasoning={message.meta.reasoning} />} display: 'flex',
{!!message.meta.retrieved?.length && <RetrievedView retrieved={message.meta.retrieved} />} alignItems: 'center',
{!!message.meta.toolCalls?.length && <ToolCallView calls={message.meta.toolCalls} />} gap: 8,
marginBottom: 6
}}>
<span style={{
fontSize: 13,
fontWeight: 500,
color: 'var(--color-text)'
}}>
</span>
</div>
<div className={`bubble ${message.role}`}>
{message.content.includes('![image](') ? (
<Markdown>{message.content}</Markdown>
) : (
<span dangerouslySetInnerHTML={{
__html: message.content.replace(/@([^\s]+)/g, '<span class="mention">@$1</span>')
}} />
)}
</div>
</div>
{/* 用户: 头像在右侧 */}
<Avatar style={{ flexShrink: 0, marginTop: 2, backgroundColor: '#1890ff' }} size={36}></Avatar>
</div> </div>
)} )}
</div> </div>