show assistant replies below avatar and profile name
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

This commit is contained in:
2026-10-24 22:45:16 +08:00
parent a54913efd2
commit d3b3a0833e
3 changed files with 81 additions and 7 deletions
@@ -0,0 +1,11 @@
---
date: 2026-10-24
pr: pending
feature: Assistant reply layout
impact: Assistant reply content starts on a new row below an avatar/profile-name header with aligned left edges on desktop and mobile. Chat events and data are unchanged.
---
The assistant message body now shows its avatar and profile name on one row,
with reply content below that header. Long names truncate with a full-name tooltip. Existing
message width limits, Markdown rendering and actions are retained. Browser checks
cover vertical placement, left alignment and overflow at 1440, 900 and 390 pixels.
@@ -1012,13 +1012,15 @@ onBeforeUnmount(() => {
</template>
<template v-else>
<div class="msg-body">
<ProfileAvatar
v-if="message.role === 'assistant'"
class="msg-avatar"
:name="assistantProfileName"
:avatar="assistantProfileAvatar"
:size="40"
/>
<div v-if="message.role === 'assistant'" class="msg-agent-header">
<ProfileAvatar
class="msg-avatar"
:name="assistantProfileName"
:avatar="assistantProfileAvatar"
:size="40"
/>
<span class="msg-profile-name" :title="assistantProfileName">{{ assistantProfileName }}</span>
</div>
<div class="msg-content" :class="message.role">
<div
class="message-bubble"
@@ -1317,11 +1319,30 @@ onBeforeUnmount(() => {
gap: 8px;
.msg-body {
flex-direction: column;
max-width: 80%;
position: relative;
z-index: 1;
}
.msg-agent-header {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
max-width: 100%;
}
.msg-profile-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: $text-secondary;
font-size: 13px;
font-weight: 600;
}
.msg-avatar {
width: 40px;
height: 40px;
+42
View File
@@ -102,3 +102,45 @@ test('opens the session model dialog and the session search from the chat header
await page.getByRole('button', { name: /search/i }).first().click()
await expect(page.getByPlaceholder(/search/i).first()).toBeVisible()
})
test('assistant replies start below the avatar and profile name on desktop and mobile', async ({ page }, testInfo) => {
await authenticate(page, TEST_ACCESS_KEY, 'research')
await mockHermesApi(page)
await mockChatSocket(page)
await page.goto('/#/hermes/chat')
await sendChatMessage(page, '检查一下服务状态')
const { sessionId } = await waitForRunSessionId(page)
await page.evaluate((sid) => {
const socket = (window as any).__PW_CHAT_SOCKET__.latest
socket.__trigger('message.delta', {
event: 'message.delta', session_id: sid, run_id: 'avatar-layout',
delta: 'UI 收到,通信正常。\n\n这台机器状态:Hermes Web UI 跑在 8648 端口,一切照旧。有活直接说,我随时待命。',
})
socket.__trigger('run.completed', { event: 'run.completed', session_id: sid, run_id: 'avatar-layout' })
}, sessionId)
const message = page.locator('.message.assistant').last()
const avatar = message.locator('.msg-avatar')
const profileName = message.locator('.msg-profile-name')
await expect(profileName).toHaveText('research')
const bubble = message.locator('.message-bubble')
await expect(bubble).toContainText('UI 收到,通信正常。')
for (const width of [1440, 900, 390]) {
await page.setViewportSize({ width, height: 900 })
await expect.poll(async () => {
const image = await avatar.boundingBox()
const reply = await bubble.boundingBox()
return image && reply ? reply.y - (image.y + image.height) : -1
}).toBeGreaterThanOrEqual(6)
const image = (await avatar.boundingBox())!
const reply = (await bubble.boundingBox())!
const name = (await profileName.boundingBox())!
expect(name.x).toBeGreaterThanOrEqual(image.x + image.width + 6)
expect(Math.abs(name.y + name.height / 2 - image.y - image.height / 2)).toBeLessThan(2)
expect(name.y + name.height).toBeLessThan(reply.y)
expect(Math.abs(reply.x - image.x)).toBeLessThan(1)
expect(reply.x + reply.width).toBeLessThanOrEqual(width)
expect(await message.evaluate(element => element.scrollWidth <= element.clientWidth + 1)).toBe(true)
if (width === 1440) await message.screenshot({ path: testInfo.outputPath('assistant-below-avatar.png'), animations: 'disabled' })
}
})