fix: 修复聊天窗口布局 - AGENT头像在左侧,用户头像在右侧
parent
345a055d1a
commit
32cb1c9d92
|
|
@ -70,71 +70,83 @@ export default function ChatBody(props: {
|
|||
|
||||
{streaming.active && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
{(() => {
|
||||
const streamingAgentId = streaming.targetAgentId || currentAgentId;
|
||||
const streamingAgent = agentList.find(a => a.id === streamingAgentId);
|
||||
if (streamingAgent) {
|
||||
return (
|
||||
<div className="message-agent-header" style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
marginLeft: 2
|
||||
}}>
|
||||
<Avatar src={streamingAgent.avatar} size={24} />
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: 'var(--color-text)'
|
||||
}}>
|
||||
{streamingAgent.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
<div className="bubble assistant">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{!!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)' }}>
|
||||
<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>
|
||||
{streaming.retryInfo.stage === 'fallback_model' ? (
|
||||
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
|
||||
{String(streaming.retryInfo.fromModel || '')} → {String(streaming.retryInfo.toModel || '')}
|
||||
</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 style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||
{(() => {
|
||||
const streamingAgentId = streaming.targetAgentId || currentAgentId;
|
||||
const streamingAgent = agentList.find(a => a.id === streamingAgentId);
|
||||
if (streamingAgent) {
|
||||
return (
|
||||
<Avatar src={streamingAgent.avatar} size={36} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{(() => {
|
||||
const streamingAgentId = streaming.targetAgentId || currentAgentId;
|
||||
const streamingAgent = agentList.find(a => a.id === streamingAgentId);
|
||||
if (streamingAgent) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 6
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: 'var(--color-text)'
|
||||
}}>
|
||||
{streamingAgent.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
<div className="bubble assistant">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{!!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)' }}>
|
||||
<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>
|
||||
{streaming.retryInfo.stage === 'fallback_model' ? (
|
||||
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
|
||||
{String(streaming.retryInfo.fromModel || '')} → {String(streaming.retryInfo.toModel || '')}
|
||||
</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 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 style={{ maxWidth: '85%' }}>
|
||||
{streaming.retrieved.length > 0 && <RetrievedView retrieved={streaming.retrieved} />}
|
||||
{streaming.toolCalls.length > 0 && <ToolCallView calls={streaming.toolCalls} liveStyle />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -43,87 +43,113 @@ export default function MessageItem(props: {
|
|||
<div
|
||||
id={'msg-' + message.id}
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
marginBottom: 20,
|
||||
padding: highlighted ? 8 : 0,
|
||||
borderRadius: highlighted ? 10 : 0,
|
||||
background: highlighted ? 'rgba(254, 243, 199, 0.6)' : 'transparent',
|
||||
transition: 'background 0.4s, padding 0.4s'
|
||||
}}
|
||||
>
|
||||
{hasAnswerAgent && (
|
||||
<div className="message-agent-header" style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
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(' ? (
|
||||
<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.role === 'assistant' ? (
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||
{/* AGENT: 头像在左侧 */}
|
||||
{hasAnswerAgent && (
|
||||
<Avatar src={answerAgent!.avatar} size={36} style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
)}
|
||||
{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 style={{ flex: 1, minWidth: 0 }}>
|
||||
{hasAnswerAgent && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 6
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: 'var(--color-text)'
|
||||
}}>
|
||||
{answerAgent!.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={`bubble ${message.role}`}>
|
||||
<Markdown>{message.content}</Markdown>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{message.role === 'assistant' && message.meta && (
|
||||
<div style={{ maxWidth: '78%' }}>
|
||||
{!!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 style={{ display: 'flex', gap: 12, alignItems: 'flex-start', justifyContent: 'flex-end' }}>
|
||||
<div style={{ flex: 1, minWidth: 0, maxWidth: '78%', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 6
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: 'var(--color-text)'
|
||||
}}>
|
||||
我
|
||||
</span>
|
||||
</div>
|
||||
<div className={`bubble ${message.role}`}>
|
||||
{message.content.includes(' ? (
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue