// 左栏:logo + 项目列表(git 图标)+ WS 状态脚 · 可折叠(折叠后仅图标) // 项目 logo:图片优先(原型无图),回退首字母色块 hsl(h 45% 60%) function ProjectLogo({ project, size = 26 }) { return ( {(project.name || '?')[0].toUpperCase()} ); } function MaestroGitIcon({ size = 16 }) { return ( ); } // 像素章鱼 mark(折叠态 logo) function MaestroMark({ scale = 2.2 }) { const ref = React.useRef(null); React.useEffect(() => { const MAP = ['.....LLLLL.....','...LLLLLLLLL...','..GGGGGGGGGGG..','..GGEEGGGEEGG..','..GGEEGGGEEGG..','..GGGGGGGGGGG..','...GGGGGGGGG...','...G..G.G..G...','...G..G.G..G...','..G...G.G...G..','..D...D.D...D..','.D...D...D...D.']; const INK = { L: '#79ec94', G: '#5fdd7d', D: '#2e6b3d', E: '#070908' }; const ctx = ref.current.getContext('2d'); MAP.forEach((row, y) => [...row].forEach((ch, x) => { if (INK[ch]) { ctx.fillStyle = INK[ch]; ctx.fillRect(x, y, 1, 1); } })); }, []); return ; } // 项目状态:运行中(青脉冲) / 暂停(faint) / 阻塞(琥珀) / 空闲(muted) function projStateMeta(state, t) { return ({ running: { color: 'var(--cyan)', pulse: true, label: t.projRunning }, paused: { color: 'var(--faint)', pulse: false, label: t.projPaused }, blocked: { color: 'var(--amber)', pulse: true, label: t.projBlocked }, idle: { color: 'var(--muted)', pulse: false, label: t.projIdle }, })[state] || { color: 'var(--muted)', pulse: false, label: t.projIdle }; } function ProjStatusDot({ meta, size = 7 }) { return ( ); } // 侧栏底部小行:图标 + 主文 + 副文(折叠时仅图标,hover 高亮) function SideRow({ icon, label, detail, onClick, collapsed, title, accent }) { const interactive = !!onClick; return (
{ e.currentTarget.style.background = 'var(--panel)'; } : undefined} onMouseLeave={interactive ? (e) => { e.currentTarget.style.background = 'transparent'; } : undefined}> {icon} {!collapsed ? ( {label} {detail ? {detail} : null} ) : null} {!collapsed && interactive ? : null}
); } const sIco = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { flex: 'none' } }; function IcoAgents() { return ; } function IcoGlobalCfg() { return ; } // 全局设置弹框:用户级默认配置(新建项目套用)。保存 / 取消,落库经 onSave。 function SettingsModal({ settings, onSave, onClose, t }) { const { Button, Input, Select } = window.MaestroDesignSystem_a6a290; const s = settings || {}; const [autonomy, setAutonomy] = React.useState(s.autonomy || 'manual'); const [concurrency, setConcurrency] = React.useState(String(s.concurrency ?? 1)); const [maxRetries, setMaxRetries] = React.useState(String(s.maxRetries ?? 2)); const [timeoutMin, setTimeoutMin] = React.useState(String(Math.round((s.timeoutMs ?? 1800000) / 60000))); const [autoPlan, setAutoPlan] = React.useState(!!s.autoApprovePlan); const [autoExec, setAutoExec] = React.useState(!!s.autoApproveExec); const [budgetUsd, setBudgetUsd] = React.useState(s.budgetUsd != null ? String(s.budgetUsd) : ''); const [budgetPeriod, setBudgetPeriod] = React.useState(s.budgetPeriod || 'month'); const [model, setModel] = React.useState(s.model || ''); React.useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); const submit = () => { onSave({ autonomy, concurrency: Number(concurrency) || 1, maxRetries: Number(maxRetries) || 0, timeoutMs: Math.max(1, Number(timeoutMin) || 30) * 60000, autoApprovePlan: autoPlan, autoApproveExec: autoExec, budgetUsd: budgetUsd.trim() === '' ? null : Number(budgetUsd), budgetPeriod, model: model.trim() === '' ? null : model.trim(), }); onClose(); }; const Toggle = ({ on, set }) => ( ); const field = (label, node) => ( ); return (
{t.gGlobalSettings}
{t.gSettingsHint}
{field(t.workMode, )} {field(t.gMaxRetries, )} {field(t.gTimeoutMin, )} {field(t.gAutoPlan, )} {field(t.gAutoExec, )} {field(t.gBudget, )} {field(t.gBudgetPeriod, )}
); } function UserRow({ user, t, collapsed, settings, onSaveSettings }) { const [open, setOpen] = React.useState(false); const [showSettings, setShowSettings] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { if (!open) return; const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [open]); const avatar = ( {user.initial} ); return (
setOpen(!open)} title={user.name + ' · ' + user.plan} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: collapsed ? '10px 0' : '10px 12px', justifyContent: collapsed ? 'center' : 'flex-start', cursor: 'pointer', transition: 'background .1s' }} onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}> {avatar} {!collapsed ? ( {user.name} {user.plan} ) : null}
{open ? (
{user.name} {user.handle}
{t.gUserPlan} · {user.plan}
{[ { label: t.gGlobalSettings, onClick: () => { setShowSettings(true); setOpen(false); }, red: false }, { label: t.gUserSettings, onClick: null, red: false }, { label: t.gUserSignOut, onClick: null, red: true }, ].map((it) => (
{ e.currentTarget.style.background = 'var(--panel-2)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{it.label}
))}
) : null} {showSettings ? setShowSettings(false)} t={t} /> : null}
); } function fmtTokens(n) { if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'; if (n >= 1e3) return Math.round(n / 1e3) + 'K'; return String(n); } function fmtUsd(n) { return '$' + (n || 0).toFixed(2); } function shortModel(m) { return (m || 'unknown').replace(/^claude-/, ''); } // 全局 AGENT 卡片(内联常驻;2×2 概览 = token/花费/运行/活跃,近 7 天窗口;明细走「详情」弹框) function AgentCard({ global, summary, t, collapsed, projects }) { const [showDetail, setShowDetail] = React.useState(false); if (collapsed) return null; return (
{t.gAgents.toUpperCase()} {t.apWeek}
{/* 概览 2×2 */}
{[ [fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)'], ['$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)'], [summary.runsWeek, t.apRuns, 'var(--ink)'], [summary.activeNow + ' / ' + global.maxAgents, t.apActive, summary.activeNow ? 'var(--cyan)' : 'var(--faint)'], ].map(([v, k, c], i) => (
{v}
{k}
))}
{/* 详情按钮 → 弹框 */} {showDetail ? setShowDetail(false)} /> : null}
); } // 用量详情弹框:按天/按月时间序列柱状图 + 分项目下钻表(token/花费/任务数/模型)。直连 /api/usage/detail。 function AgentDetailModal({ projects, t, onClose }) { const [gran, setGran] = React.useState('day'); const [data, setData] = React.useState(null); const [err, setErr] = React.useState(false); React.useEffect(() => { let alive = true; setData(null); setErr(false); fetch('/api/usage/detail?granularity=' + gran) .then((r) => r.ok ? r.json() : Promise.reject(new Error('http'))) .then((d) => { if (alive) setData(d); }) .catch(() => { if (alive) setErr(true); }); return () => { alive = false; }; }, [gran]); const nameOf = new Map((projects || []).map((p) => [p.id, p])); const series = data?.series || []; const maxCost = Math.max(...series.map((s) => s.costUsd), 1e-9); const tot = data?.total || { costUsd: 0, tokens: 0, runs: 0, tasks: 0 }; const emptyBox = (msg) =>
{msg}
; return (
{/* 标题 + 段切换 */}
{t.apDetailTitle}
{[['day', t.apByDay], ['week', t.apByWeek], ['month', t.apByMonth]].map(([g, label]) => ( ))}
{err ? emptyBox(t.apEmpty) : !data ? emptyBox('…') : ( {/* total 概览 */}
{[ [fmtUsd(tot.costUsd), t.apCost, 'var(--green)'], [fmtTokens(tot.tokens), t.apTokens, 'var(--cyan)'], [tot.runs, t.apRuns, 'var(--ink)'], [tot.tasks, t.apColTasks, 'var(--ink)'], ].map(([v, k, c], i) => (
{v}
{k}
))}
{/* 时间序列柱状图(按花费成柱,hover 看 token) */} {series.length ? (
{series.map((s) => (
))}
{series[0].bucket}{series[series.length - 1].bucket}
) :
{emptyBox(t.apEmpty)}
} {/* 分项目下钻表 */}
{t.apPerProj}
{data.byProject.length ? data.byProject.map((p) => { const pr = nameOf.get(p.projectId); const nm = pr?.name || p.projectId; const hue = pr?.hue ?? 200; return ( ); }) : }
{t.apColProject} {t.apTokens} {t.apCost} {t.apColTasks} {t.apColModels}
{nm[0].toUpperCase()} {nm} {fmtTokens(p.tokens)} {fmtUsd(p.costUsd)} {p.tasks} {p.models.map((m) => ( {shortModel(m.model)} ))}
{emptyBox(t.apEmpty)}
)}
); } function GlobalConfigRow({ global, t, collapsed }) { const { Select } = window.MaestroDesignSystem_a6a290; const [open, setOpen] = React.useState(false); const [pos, setPos] = React.useState(null); const ref = React.useRef(null); const rowRef = React.useRef(null); React.useEffect(() => { if (!open) return; const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && rowRef.current && !rowRef.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [open]); const toggle = () => { if (!open && rowRef.current) { const r = rowRef.current.getBoundingClientRect(); const W = 250; // 优先在行上方对齐左缘;折叠态贴侧栏右侧 const left = collapsed ? r.right + 6 : Math.min(r.left, window.innerWidth - W - 8); setPos({ left, bottom: window.innerHeight - r.top + 6, width: W }); } setOpen((o) => !o); }; const stat = (k, v, accent) => (
{k} {v}
); return (
} onClick={toggle} label={t.gConfig} detail={t.gConfigDetail.replace('{n}', global.maxAgents).replace('{v}', global.daemon)} title={t.gConfig} />
{open && pos ? (
{t.gConfig.toUpperCase()}
{stat('daemon', global.daemon, 'var(--green)')} {stat('port', ':' + global.port)} {stat('uptime', global.uptime)} {stat(t.gAgents, global.runningAgents + ' / ' + global.maxAgents, 'var(--cyan)')}
) : null}
); } function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onToggleCollapse, t, global, user, summary, onGlobalConfig, onReorder, settings, onSaveSettings }) { const { Button } = window.MaestroDesignSystem_a6a290; const [dragId, setDragId] = React.useState(null); const [overId, setOverId] = React.useState(null); const [showAllProjects, setShowAllProjects] = React.useState(false); const PROJ_LIMIT = 6; // 展开态下最多显示 6 个,多余折叠(折叠侧栏 rail 态显示全部小圆点,不截断) const shownProjects = (collapsed || showAllProjects) ? projects : projects.slice(0, PROJ_LIMIT); const onDrop = (targetId) => { if (dragId && dragId !== targetId && onReorder) { const ids = projects.map((x) => x.id); const from = ids.indexOf(dragId); const to = ids.indexOf(targetId); if (from > -1 && to > -1) { ids.splice(to, 0, ids.splice(from, 1)[0]); onReorder(ids); } } setDragId(null); setOverId(null); }; return ( ); } window.MaestroKitSidebar = Sidebar;