fix: 修复聊天窗口布局 - AGENT头像在左侧,用户头像在右侧
parent
345a055d1a
commit
32cb1c9d92
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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(' ? (
|
|
||||||
<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(' ? (
|
||||||
|
<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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue