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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="msg-body">
|
<div class="msg-body">
|
||||||
<ProfileAvatar
|
<div v-if="message.role === 'assistant'" class="msg-agent-header">
|
||||||
v-if="message.role === 'assistant'"
|
<ProfileAvatar
|
||||||
class="msg-avatar"
|
class="msg-avatar"
|
||||||
:name="assistantProfileName"
|
:name="assistantProfileName"
|
||||||
:avatar="assistantProfileAvatar"
|
:avatar="assistantProfileAvatar"
|
||||||
:size="40"
|
:size="40"
|
||||||
/>
|
/>
|
||||||
|
<span class="msg-profile-name" :title="assistantProfileName">{{ assistantProfileName }}</span>
|
||||||
|
</div>
|
||||||
<div class="msg-content" :class="message.role">
|
<div class="msg-content" :class="message.role">
|
||||||
<div
|
<div
|
||||||
class="message-bubble"
|
class="message-bubble"
|
||||||
@@ -1317,11 +1319,30 @@ onBeforeUnmount(() => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.msg-body {
|
.msg-body {
|
||||||
|
flex-direction: column;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
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 {
|
.msg-avatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 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 page.getByRole('button', { name: /search/i }).first().click()
|
||||||
await expect(page.getByPlaceholder(/search/i).first()).toBeVisible()
|
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