// App shell — light theme, Chinese strings const SHELL_NAV = [ { id: 'overview', label: '總覽', icon: 'M3 12h18M3 6h18M3 18h18' }, { id: 'brands', label: '品牌', icon: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z' }, { id: 'conversations', label: '對話', icon: 'M4 4h16v12H7l-3 3z' }, { id: 'cost', label: '費用分析', icon: 'M3 21l5-9 4 4 8-12' }, { id: 'tools', label: '工具品質', icon: 'M14 4l6 6-10 10H4v-6z', soon: true }, { id: 'alerts', label: '警示規則', icon: 'M12 3l9 16H3z', soon: true }, { id: 'audit', label: '稽核紀錄', icon: 'M5 4h14v16H5zM8 8h8M8 12h8M8 16h5', soon: true }, ]; const TIME_PRESETS = [ { id: 'today', label: '今天' }, { id: '7d', label: '近 7 天' }, { id: '30d', label: '近 30 天' }, { id: 'month', label: '本月' }, { id: 'last', label: '上月' }, { id: 'custom', label: '自訂' }, ]; const CHANNEL_OPTS = [ { id: 'all', label: '全部' }, { id: 'dashboard', label: '管理後台' }, { id: 'line', label: 'LINE' }, ]; const MODEL_OPTS = [ { id: 'all', label: '全部' }, { id: 'anthropic', label: 'Anthropic' }, { id: 'openai', label: 'OpenAI' }, { id: 'claude-haiku-4-5', label: 'claude-haiku-4-5' }, { id: 'claude-sonnet-4-5', label: 'claude-sonnet-4-5' }, { id: 'claude-opus-4-1', label: 'claude-opus-4-1' }, { id: 'gpt-4.1', label: 'gpt-4.1' }, { id: 'gpt-4.1-mini', label: 'gpt-4.1-mini' }, ]; function Icon({ d, size = 14, stroke = 'currentColor', sw = 1.5 }) { return ( ); } function TopBar({ env = 'staging', adminUser }) { const envCfg = { prod: { color: '#DC2626', label: '正式' }, staging: { color: '#B45309', label: '預備' }, dev: { color: '#15803D', label: '開發' }, }[env]; // 從 /api/me 拿到的真實員工資料;fallback 給 design-time 預覽 const name = (adminUser && adminUser.name) || '—'; const email = (adminUser && adminUser.email) || ''; const initials = (() => { if (!adminUser || !adminUser.name) return '··'; // 中文名取首字 + 英文取首字(如「謝湘漪 Vivian」→「謝V」) const parts = String(adminUser.name).trim().split(/\s+/); if (parts.length >= 2) { const last = parts[parts.length - 1]; return parts[0].charAt(0) + (last.match(/[A-Za-z]/) ? last.charAt(0).toUpperCase() : ''); } return String(adminUser.name).slice(0, 2); })(); return (
O
Ocard / Agent 監控後台
環境:{envCfg.label}
{name}
{email &&
{email}
}
{initials}
); } function LeftRail({ route, setRoute }) { return ( ); } function FilterBar({ filters, setFilters, brandsList }) { const [brandOpen, setBrandOpen] = React.useState(false); const [brandQuery, setBrandQuery] = React.useState(''); const filteredBrands = brandsList.filter(b => b.name.toLowerCase().includes(brandQuery.toLowerCase()) || b.id.includes(brandQuery.toLowerCase()) ); const selectedCount = filters.brands.length; return (
{TIME_PRESETS.map(p => { const isCustom = p.id === 'custom'; return ( ); })}
{/* 以下三個 dropdown 後端尚未支援篩選參數(brands / channel / model),先 disable 以免誤導 */}
{false && brandOpen && (
setBrandQuery(e.target.value)} placeholder="搜尋品牌..." style={{ width: '100%', padding: '6px 8px', fontSize: 12, background: '#F7F7F5', border: '1px solid #E5E5E0', borderRadius: 4, color: '#17171A', outline: 'none', marginBottom: 4, }} />
{filteredBrands.slice(0, 30).map(b => { const checked = filters.brands.includes(b.id); return ( ); })}
{filters.brands.length > 0 && ( )}
)}
setFilters({ ...filters, channel: v })} disabled hint="即將推出:依渠道篩選" /> setFilters({ ...filters, model: v })} disabled hint="即將推出:依模型篩選" />
?range={filters.range}&brands={filters.brands.length || 'all'}&channel={filters.channel}&model={filters.model}
); } function Dropdown({ label, value, options, onChange, disabled, hint }) { const [open, setOpen] = React.useState(false); const cur = options.find(o => o.id === value) || options[0]; return (
{open && (
{options.map(o => ( ))}
)}
); } window.Icon = Icon; window.TopBar = TopBar; window.LeftRail = LeftRail; window.FilterBar = FilterBar;