feat(ui): ReplyPier 品牌、中文界面与前端构建产物

替换品牌文案与图标,补齐窄屏布局和加载失败提示,确认主要旅程可操作。
This commit is contained in:
WangShen007 committed 2026-10-15 20:25:00 +08:00
1 parent 2adf288cd2
commit ba91abc4b2
77 files changed
+2916

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
/*
Copyright 2024-2026 ChatterMate
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Procedural "aurora" gradient orbs used as an avatar alternative to an uploaded photo.
// Colors are hardcoded hex (not CSS vars) so the orb renders identically in the dashboard,
// the preview panel, and the isolated widget bundle (which has no access to app design tokens).
interface OrbPalette {
stops: string
glow: string
}
// Each entry is a visibly distinct colored orb the user can choose from.
const ORB_PALETTES: OrbPalette[] = [
{ stops: '#9D8CFF, #5FE3D6, #C9F24E', glow: 'rgba(157,140,255,0.45)' }, // aurora (default)
{ stops: '#FF8A73, #9D8CFF, #5FE3D6', glow: 'rgba(255,138,115,0.40)' }, // coral
{ stops: '#5FE3D6, #C9F24E, #9D8CFF', glow: 'rgba(95,227,214,0.40)' }, // teal
{ stops: '#C9F24E, #5FE3D6, #FF8A73', glow: 'rgba(201,242,78,0.35)' }, // lime
{ stops: '#6EA8FF, #9D8CFF, #5FE3D6', glow: 'rgba(110,168,255,0.42)' }, // blue
{ stops: '#FF7AC6, #9D8CFF, #6EA8FF', glow: 'rgba(255,122,198,0.42)' }, // pink
{ stops: '#FF8A73, #FFC857, #FF7AC6', glow: 'rgba(255,200,87,0.40)' }, // sunset
{ stops: '#7C5CFF, #B388FF, #5FE3D6', glow: 'rgba(124,92,255,0.45)' }, // violet
{ stops: '#0EA5A5, #5FE3D6, #C9F24E', glow: 'rgba(14,165,165,0.40)' }, // emerald
{ stops: '#F34611, #FF8A73, #FFC857', glow: 'rgba(243,70,17,0.38)' }, // ember
]
// Number of selectable orb variants (for rendering the picker grid).
export const ORB_PALETTE_COUNT = ORB_PALETTES.length
// Stable palette index from a seed (agent name) so the same agent always gets the same orb
// when no variant has been explicitly chosen.
export const orbIndexForSeed = (seed: string): number =>
(seed || '').split('').reduce((a, c) => a + c.charCodeAt(0), 0) % ORB_PALETTES.length
// Inline style object for a specific orb variant by index.
export const getOrbStyleAt = (index: number): Record<string, string> => {
const p = ORB_PALETTES[((index % ORB_PALETTES.length) + ORB_PALETTES.length) % ORB_PALETTES.length]
return {
background: `
radial-gradient(circle at 32% 28%, rgba(255,255,255,0.22) 0%, transparent 42%),
radial-gradient(circle at 68% 72%, rgba(0,0,0,0.25) 0%, transparent 38%),
radial-gradient(ellipse at 50% 50%, ${p.stops})
`.trim(),
boxShadow: `0 4px 28px ${p.glow}, inset 0 1px 0 rgba(255,255,255,0.15)`,
borderRadius: '50%',
}
}
// Inline style for the orb, keyed by a seed (used as the stable default).
export const getOrbStyle = (seed: string): Record<string, string> => getOrbStyleAt(orbIndexForSeed(seed))
// Resolve the orb style for an agent: an explicitly chosen variant wins, otherwise
// fall back to the seed-derived default. `variant` is whatever is stored in
// customization_metadata.orb_variant (may be undefined / non-numeric).
export const resolveOrbStyle = (seed: string, variant: unknown): Record<string, string> => {
const idx = typeof variant === 'number' && Number.isFinite(variant) ? variant : orbIndexForSeed(seed)
return getOrbStyleAt(idx)
}
// Render an orb variant as a self-contained SVG data URI. Storing this in
// `photo_url` lets every existing avatar render site (agent list, widget header,
// previews) show the orb with no orb-specific logic — they just render the image.
export const orbSvgDataUri = (seed: string, variant?: unknown): string => {
const idx = typeof variant === 'number' && Number.isFinite(variant) ? variant : orbIndexForSeed(seed)
const p = ORB_PALETTES[((idx % ORB_PALETTES.length) + ORB_PALETTES.length) % ORB_PALETTES.length]
const [c1, c2, c3] = p.stops.split(',').map((s) => s.trim())
const svg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
'<defs>' +
'<radialGradient id="o" cx="50%" cy="50%" r="70%">' +
`<stop offset="0%" stop-color="${c1}"/>` +
`<stop offset="50%" stop-color="${c2}"/>` +
`<stop offset="100%" stop-color="${c3}"/>` +
'</radialGradient>' +
'<radialGradient id="h" cx="32%" cy="28%" r="45%">' +
'<stop offset="0%" stop-color="#ffffff" stop-opacity="0.45"/>' +
'<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>' +
'</radialGradient>' +
'<radialGradient id="s" cx="68%" cy="72%" r="42%">' +
'<stop offset="0%" stop-color="#000000" stop-opacity="0.3"/>' +
'<stop offset="100%" stop-color="#000000" stop-opacity="0"/>' +
'</radialGradient>' +
'</defs>' +
'<circle cx="50" cy="50" r="50" fill="url(#o)"/>' +
'<circle cx="50" cy="50" r="50" fill="url(#h)"/>' +
'<circle cx="50" cy="50" r="50" fill="url(#s)"/>' +
'</svg>'
return `data:image/svg+xml,${encodeURIComponent(svg)}`
}
// Terminal-style avatar mark: a monospace ">" prompt on a dark, FULL-BLEED tile tinted
// with the accent (matches the Terminal chat theme). Full-bleed so it fully covers the
// avatar container at every render site — circular (header/message) or square — with no
// transparent corners. Stored in `photo_url` exactly like the orb, so every avatar
// render site shows it with no mark-specific logic.
export const TERMINAL_MARK_ACCENT = '#C9F24E'
// Only accept simple, safe colour tokens (hex / rgb[a] / hsl[a] / named) before
// embedding into the SVG, so a stray quote in a stored value can't break out of the
// attribute. Anything else falls back to the Terminal lime.
const SAFE_COLOR = /^(#[0-9a-f]{3,8}|rgba?\([\d.,\s%]+\)|hsla?\([\d.,\s%]+\)|[a-z]+)$/i
export const terminalMarkSvgDataUri = (accent: string = TERMINAL_MARK_ACCENT): string => {
const c = accent && SAFE_COLOR.test(accent.trim()) ? accent.trim() : TERMINAL_MARK_ACCENT
const svg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
// Full-bleed dark base guarantees coverage in any avatar shape.
'<rect width="100" height="100" fill="#0B0C10"/>' +
`<rect width="100" height="100" fill="${c}" fill-opacity="0.16"/>` +
// Inset accent frame for the terminal look.
`<rect x="14" y="14" width="72" height="72" rx="14" fill="none" stroke="${c}" stroke-opacity="0.6" stroke-width="4"/>` +
`<text x="50" y="52" font-family="'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" font-size="52" font-weight="700" fill="${c}" text-anchor="middle" dominant-baseline="central">&gt;</text>` +
'</svg>'
return `data:image/svg+xml,${encodeURIComponent(svg)}`
}