// 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 (
環境:{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;