show assistant replies below avatar and profile name
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user