/** * Shared UI primitives for AgentHub HTML pages. * * Constraints: * - Dependency-free: only string/template helpers, no npm UI libs. * - No emojis: inline SVG/CSS only. * - Self-contained: pages import this and inline the returned CSS/JS. */ import { TaskStatus as TaskStatusSchema } from '../core/schema.js'; type TaskStatus = 'open' | 'in_progress' | 'review' | 'done' | 'cancelled'; /** CSS variables block matching the AgentHub dark design spec. */ export function designTokensCss(): string { return ` :root { --bg: #0F172A; --surface: #161B22; --raised: #1E293B; --border: #30363D; --text: #F8FAFC; --muted: #94A3B8; --accent: #58A6FF; --green: #22C55E; --status-open: #8B949E; --status-in_progress: #58A6FF; --status-review: #D29922; --status-done: #22C55E; --status-cancelled: #6E7681; --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --font-mono: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, Monaco, Consolas, monospace; } * { box-sizing: border-box; } html, body { margin: 0; height: 100%; } body { background: var(--bg); color: var(--text); font: 14px/1.5 var(--font-sans); padding: 16px 20px 32px; } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } `; } const AGENT_PALETTE: Record = { claude: { color: '#D97757', initial: 'C' }, codex: { color: '#10A37F', initial: 'Cx' }, kimi: { color: '#7C3AED', initial: 'K' }, 'windows-claude': { color: '#2563EB', initial: 'W' }, backyard: { color: '#64748B', initial: 'B' }, }; function hashString(str: string): number { let h = 0; for (let i = 0; i < str.length; i++) { h = (h << 5) - h + str.charCodeAt(i); h |= 0; } return Math.abs(h); } function deterministicColor(name: string): string { const colors = ['#DC2626', '#EA580C', '#D97706', '#65A30D', '#0891B2', '#2563EB', '#7C3AED', '#DB2777']; return colors[hashString(name) % colors.length]; } export interface AgentAvatarOptions { /** Render an extra ring for the architect role. */ architectRing?: boolean; size?: number; } /** * Render a round agent avatar chip with per-agent accent color and initials. * No external images, no emojis. */ export function agentAvatar( name: string | undefined, options: AgentAvatarOptions = {}, ): string { const resolvedName = name?.toLowerCase() ?? ''; const spec = AGENT_PALETTE[resolvedName] ?? { color: deterministicColor(resolvedName || 'unknown'), initial: (name ?? '?').slice(0, 1).toUpperCase(), }; const { architectRing = false, size = 22 } = options; const fontSize = Math.round(size * 0.45); const ring = architectRing ? `box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px ${spec.color};` : ''; return `${escapeHtml(spec.initial)}`; } /** * Provider/company brand metadata + official Simple Icons SVG paths (verified, * 24x24 viewBox). Used to group + badge agents by the model behind them. */ const PROVIDERS: Record = { anthropic: { name: 'Anthropic', color: '#D97757', path: 'M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z', }, openai: { name: 'OpenAI', color: '#10A37F', path: 'M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z', }, moonshot: { name: 'Moonshot', color: '#7C3AED', path: 'm1.053 16.91 9.538 2.55a21 20.981 0 0 0 .06 2.031l5.956 1.592a12 11.99 0 0 1-15.554-6.172m-1.02-5.79 11.352 3.035a21 20.981 0 0 0-.469 2.01l10.817 2.89a12 11.99 0 0 1-1.845 2.004L.658 15.918a12 11.99 0 0 1-.625-4.796m1.593-5.146L13.573 9.17a21 20.981 0 0 0-1.01 1.874l11.297 3.02a21 20.981 0 0 1-.67 2.362l-11.55-3.087L.125 10.26a12 11.99 0 0 1 1.499-4.285ZM6.067 1.58l11.285 3.016a21 20.981 0 0 0-1.688 1.719l7.824 2.091a21 20.981 0 0 1 .513 2.664L2.107 5.218a12 11.99 0 0 1 3.96-3.638M21.68 4.866 7.222 1.003A12 11.99 0 0 1 21.68 4.866', }, }; /** Brand metadata for a provider kind, or null if unknown. */ export function providerMeta(kind?: string): { name: string; color: string } | null { if (!kind) return null; const p = PROVIDERS[kind]; return p ? { name: p.name, color: p.color } : null; } /** Inline brand logo SVG for a provider kind, tinted with its brand color. */ export function providerLogo(kind: string | undefined, size = 20): string { const p = kind ? PROVIDERS[kind] : undefined; if (!p) return ''; return ``; } /** Inline status pill for a task status. */ export function statusPill(status: TaskStatus): string { const labels: Record = { open: 'Open', in_progress: 'In Progress', review: 'Review', done: 'Done', cancelled: 'Cancelled', }; const colorVar = `--status-${status}`; return `${escapeHtml(labels[status])}`; } /** * CSS snippet that injects RGB versions of status colors so statusPill can use * rgba() backgrounds/borders. Include this once in the page