feat(ui): refresh dashboard and paper boat branding

This commit is contained in:
WangShen007 committed 2026-10-22 17:44:36 +08:00
1 parent 308332eb94
commit 1752a5b685
93 files changed
+11134 -7543

No files matched your search

@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import { ref } from 'vue'
import {
getOrbStyleAt, ORB_PALETTE_COUNT, PAPER_AVATAR_NAMES,
orbIndexForSeed, orbSvgDataUri, resolveAvatarImage, resolveOrbStyle,
terminalMarkSvgDataUri,
} from '@/utils/orb'
import { useWidgetStyles } from '@/composables/useWidgetStyles'
import type { AgentCustomization } from '@/types/widget'
const decode = (uri: string) => decodeURIComponent(uri.replace('data:image/svg+xml,', ''))
describe('paper-craft avatars', () => {
it('ships ten distinct flat vectors under the existing variant indexes', () => {
expect(ORB_PALETTE_COUNT).toBe(10)
expect(PAPER_AVATAR_NAMES).toHaveLength(10)
const variants = Array.from({ length: 10 }, (_, i) => orbSvgDataUri('support', i))
expect(new Set(variants).size).toBe(10)
for (const uri of variants) {
const svg = new DOMParser().parseFromString(decode(uri), 'image/svg+xml')
expect(svg.querySelector('parsererror')).toBeNull()
expect(svg.documentElement.getAttribute('viewBox')).toBe('0 0 100 100')
expect(svg.querySelector('radialGradient, linearGradient, script, image')).toBeNull()
expect(svg.querySelector('path')).not.toBeNull()
}
})
it('uses the exact same artwork for CSS avatar slots and img slots', () => {
for (let i = 0; i < 10; i++) {
expect(getOrbStyleAt(i).background).toContain(orbSvgDataUri('support', i))
expect(resolveOrbStyle('support', i)).toEqual(getOrbStyleAt(i))
}
})
it('retains stable seeds and normalizes out-of-range stored indexes', () => {
expect(orbSvgDataUri('support')).toBe(orbSvgDataUri('', orbIndexForSeed('support')))
expect(orbSvgDataUri('', -1)).toBe(orbSvgDataUri('', 9))
expect(orbSvgDataUri('', 12)).toBe(orbSvgDataUri('', 2))
expect(orbSvgDataUri('', 2.5)).toBe(orbSvgDataUri('', 2))
for (const invalid of [NaN, Infinity, '2', null]) {
expect(orbSvgDataUri('support', invalid)).toBe(orbSvgDataUri('support'))
}
})
it('replaces an old stored orb from metadata, not from the old data URI', () => {
expect(resolveAvatarImage('support', {
photo_url: 'data:image/svg+xml,old-gradient',
customization_metadata: { avatar_style: 'orb', orb_variant: 0 },
})).toBe(orbSvgDataUri('support', 0))
})
it.each([
'/api/v1/uploads/agents/a.png',
'https://cdn.example/a.png?signature=preserved',
'data:image/png;base64,customer-photo',
'blob:https://example.com/preview',
])('preserves an uploaded image: %s', (photo) => {
expect(resolveAvatarImage('support', { photo_url: photo })).toBe(photo)
expect(resolveAvatarImage('support', {
photo_url: photo, customization_metadata: { avatar_style: 'photo', orb_variant: 0 },
})).toBe(photo)
})
it('provides a complete avatar when no photo exists', () => {
expect(resolveAvatarImage('support')).toBe(orbSvgDataUri('support'))
})
it('retains the terminal option and rejects markup in its color', () => {
const terminal = terminalMarkSvgDataUri()
expect(decode(terminal)).toContain('#8BB49C')
expect(terminalMarkSvgDataUri('\"/><script>alert(1)</script>')).toBe(terminal)
expect(resolveAvatarImage('support', {
photo_url: terminal, customization_metadata: { avatar_style: 'terminal' },
})).toBe(terminal)
})
it('updates the real widget image reactively and preserves its runtime upload origin', () => {
const customization = ref<AgentCustomization>({
photo_url: 'data:image/svg+xml,old-gradient',
customization_metadata: { avatar_style: 'orb', orb_variant: 0 },
})
const { photoUrl } = useWidgetStyles(customization, ref('support'))
expect(photoUrl.value).toBe(orbSvgDataUri('support', 0))
customization.value = {
photo_url: 'https://cdn.example/customer.png',
customization_metadata: { avatar_style: 'photo' },
}
expect(photoUrl.value).toBe('https://cdn.example/customer.png')
})
})