feat: complete grounded digital avatar chat experience
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
pickAvatarId,
|
||||
unwrapListData,
|
||||
} from '../src/utils/avatar-page-data.js'
|
||||
import { renderChatMarkdownCharacters } from '../src/utils/chat-markdown.js'
|
||||
|
||||
assert.deepEqual(unwrapListData([{ id: 'a1' }]), [{ id: 'a1' }], 'unwrapListData should return raw arrays')
|
||||
assert.deepEqual(
|
||||
@@ -29,6 +30,23 @@ assert.equal(
|
||||
)
|
||||
assert.equal(pickAvatarId('', []), null, 'pickAvatarId should return null when no avatar exists')
|
||||
|
||||
const boldReply = renderChatMarkdownCharacters('请注意:**不能自行诊断或随意用药**。')
|
||||
assert.equal(
|
||||
boldReply.map((character) => character.text).join(''),
|
||||
'请注意:不能自行诊断或随意用药。',
|
||||
'chat markdown should hide bold markers'
|
||||
)
|
||||
assert.equal(
|
||||
boldReply.filter((character) => character.bold).map((character) => character.text).join(''),
|
||||
'不能自行诊断或随意用药',
|
||||
'chat markdown should style bold text'
|
||||
)
|
||||
assert.equal(
|
||||
renderChatMarkdownCharacters('****重点****').map((character) => character.text).join(''),
|
||||
'重点',
|
||||
'chat markdown should tolerate repeated bold markers'
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
normalizeAvatarEditForm({
|
||||
name: '我的分身',
|
||||
@@ -36,7 +54,19 @@ assert.deepEqual(
|
||||
description: '描述',
|
||||
status: 'inactive',
|
||||
photoUrl: 'https://img.example/avatar.png',
|
||||
config: { replyStyle: 'friendly', creativity: 72, rigor: 88, humor: 16, responseLength: 'short', systemPrompt: '不要编造', autoReply: false },
|
||||
config: {
|
||||
replyStyle: 'friendly',
|
||||
creativity: 72,
|
||||
rigor: 88,
|
||||
humor: 16,
|
||||
responseLength: 'short',
|
||||
systemPrompt: '不要编造',
|
||||
profession: '医生',
|
||||
position: '主任医师',
|
||||
organization: '测试医院',
|
||||
organizationAddress: '测试路 1 号',
|
||||
autoReply: false
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: '我的分身',
|
||||
@@ -50,6 +80,10 @@ assert.deepEqual(
|
||||
humor: 16,
|
||||
responseLength: 'short',
|
||||
systemPrompt: '不要编造',
|
||||
profession: '医生',
|
||||
position: '主任医师',
|
||||
organization: '测试医院',
|
||||
organizationAddress: '测试路 1 号',
|
||||
autoReply: false,
|
||||
},
|
||||
'normalizeAvatarEditForm should map API avatars into edit form state'
|
||||
@@ -68,6 +102,10 @@ assert.deepEqual(
|
||||
humor: 25,
|
||||
responseLength: 'medium',
|
||||
systemPrompt: '回答简洁',
|
||||
profession: '医生',
|
||||
position: '主任医师',
|
||||
organization: '测试医院',
|
||||
organizationAddress: '测试路 1 号',
|
||||
autoReply: true,
|
||||
}),
|
||||
{
|
||||
@@ -83,6 +121,10 @@ assert.deepEqual(
|
||||
humor: 25,
|
||||
responseLength: 'medium',
|
||||
systemPrompt: '回答简洁',
|
||||
profession: '医生',
|
||||
position: '主任医师',
|
||||
organization: '测试医院',
|
||||
organizationAddress: '测试路 1 号',
|
||||
autoReply: true,
|
||||
},
|
||||
},
|
||||
@@ -94,6 +136,45 @@ assert.match(knowledgeView, /文档知识库/, 'knowledge page should expose the
|
||||
assert.match(knowledgeView, /标准问答对/, 'knowledge page should expose the QA tab')
|
||||
assert.match(knowledgeView, /activeTab/, 'knowledge page should switch active tabs')
|
||||
assert.match(knowledgeView, /accept="\.md,\.txt,\.pdf,\.doc,\.docx,\.xlsx"/, 'knowledge page should accept md and txt')
|
||||
assert.match(knowledgeView, /table-scroll/, 'knowledge page should use a scrollable table wrapper')
|
||||
assert.match(knowledgeView, /mobile-card-list/, 'knowledge page should render mobile-first card lists')
|
||||
assert.match(knowledgeView, /knowledge-card/, 'knowledge page should expose document and QA cards')
|
||||
|
||||
const chatView = fs.readFileSync(path.resolve('src/views/AvatarChat.vue'), 'utf8')
|
||||
assert.match(chatView, /avatar\?\.photoUrl/, 'chat should render the active avatar photo when available')
|
||||
assert.match(chatView, /userAvatarUrl/, 'chat should render the logged-in user photo when available')
|
||||
assert.match(chatView, /avatarStatus/, 'chat should synchronize the visible status indicator with avatar status')
|
||||
assert.match(chatView, /document\.title = avatar\.value/, 'chat should use the avatar name as the page title')
|
||||
assert.match(chatView, /position: sticky/, 'chat header should remain visible while the message list scrolls')
|
||||
assert.match(chatView, /typing-character/, 'chat replies should animate one character at a time')
|
||||
assert.match(chatView, /renderChatMarkdownCharacters/, 'chat replies should render markdown as safe web text')
|
||||
assert.match(chatView, /markdown-bold/, 'chat replies should style markdown emphasis without showing markers')
|
||||
assert.match(chatView, /streamAvatarChat/, 'private chat should consume SSE response chunks')
|
||||
assert.match(chatView, /streamPublicAvatarChat/, 'public chat should consume SSE response chunks')
|
||||
assert.match(chatView, /scrollDuringStream/, 'streaming replies should throttle scrolling to animation frames')
|
||||
assert.match(chatView, /typing-character\.newline/, 'streaming replies should render sentence line breaks')
|
||||
assert.match(chatView, /let attached = false/, 'assistant bubble should wait for the first streamed text chunk')
|
||||
assert.match(chatView, /reactive<DisplayMessage>/, 'every streamed character should update through a reactive reply object')
|
||||
assert.doesNotMatch(chatView, /你好,我是\{\{/, 'chat welcome card should not introduce the avatar by name')
|
||||
assert.doesNotMatch(chatView, /\/\[。!?;\]\/\.test\(character\)/, 'chat should not force a line break after every sentence')
|
||||
assert.match(chatView, /previous === '\\n'/, 'streaming text should collapse whitespace at line boundaries')
|
||||
assert.match(chatView, /welcome-avatar/, 'chat welcome should use the active avatar image instead of a generic icon')
|
||||
assert.doesNotMatch(chatView, /我会优先参考标准问答和知识库/, 'chat welcome should not expose internal answer sources')
|
||||
assert.match(chatView, /welcome-description/, 'chat welcome should render the avatar description')
|
||||
assert.doesNotMatch(chatView, /介绍一下你自己/, 'chat welcome should not contain fixed starter questions')
|
||||
|
||||
const editView = fs.readFileSync(path.resolve('src/views/AvatarEdit.vue'), 'utf8')
|
||||
assert.match(editView, />分身微调</, 'avatar edit page should use the requested title')
|
||||
assert.match(editView, /uploadAvatarPhoto/, 'avatar edit page should upload a clicked replacement photo')
|
||||
assert.doesNotMatch(editView, />头像链接</, 'avatar edit page should not expose a photo URL input')
|
||||
for (const field of ['profession', 'position', 'organization', 'organizationAddress']) {
|
||||
assert.match(editView, new RegExp(`formData\\.${field}`), `avatar edit page should expose ${field}`)
|
||||
}
|
||||
|
||||
const manageView = fs.readFileSync(path.resolve('src/views/AvatarManage.vue'), 'utf8')
|
||||
assert.match(manageView, /shareAvatar/, 'avatar management should offer a share action')
|
||||
assert.match(manageView, /createAvatarShareLink/, 'share action should create a public share link')
|
||||
|
||||
const router = fs.readFileSync(path.resolve('src/router/index.ts'), 'utf8')
|
||||
assert.match(router, /path: '\/share\/:shareToken'/, 'router should expose a public chat route')
|
||||
|
||||
console.log('avatar-page-data tests passed')
|
||||
|
||||
Reference in New Issue
Block a user