feat: initial public release (MAESTRO)

This commit is contained in:
oss-sync
2026-06-03 05:08:00 +00:00
commit f5c7666f6b
823 changed files with 184150 additions and 0 deletions
@@ -0,0 +1,128 @@
// ChatPane — mirrors ui/src/components/chat/* with user/ask/result/progress bubbles
function Bubble({ role, children, footer }) {
const isUser = role === 'user';
const style = {
maxWidth: '85%',
padding: '10px 14px',
borderRadius: 16,
fontSize: 13,
lineHeight: 1.55,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
};
if (isUser) {
Object.assign(style, { background: '#2563eb', color: '#fff', borderBottomRightRadius: 4, alignSelf: 'flex-end' });
} else if (role === 'ask') {
Object.assign(style, { background: '#fef9c3', color: '#854d0e', border: '1px solid #fde68a', borderBottomLeftRadius: 4 });
} else if (role === 'result') {
Object.assign(style, { background: '#ecfdf5', color: '#065f46', border: '1px solid #a7f3d0', borderBottomLeftRadius: 4 });
} else {
Object.assign(style, { background: '#fff', color: '#0f172a', border: '1px solid #e2e8f0', borderBottomLeftRadius: 4 });
}
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: isUser ? 'flex-end' : 'flex-start', gap: 4 }}>
<div style={style}>{children}</div>
{footer && <div style={{ fontSize: 10, color: '#94a3b8' }}>{footer}</div>}
</div>
);
}
function ProgressBubble({ text }) {
return (
<div style={{
alignSelf: 'flex-start', background: '#f1f5f9', color: '#475569',
padding: '8px 12px', borderRadius: 12, fontSize: 12,
display: 'inline-flex', alignItems: 'center', gap: 8,
}}>
<Spinner />
<span>{text}</span>
</div>
);
}
function ChatHeader({ task, onOpenDetail, detailOpen }) {
return (
<div style={{
flexShrink: 0, padding: '12px 16px', borderBottom: '1px solid #e2e8f0', background: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
}}>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 10, fontFamily: 'IBM Plex Mono, monospace', color: '#94a3b8', letterSpacing: '.08em' }}>
TASK #{task.id}
</div>
<div style={{ fontSize: 15, fontWeight: 700, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{task.title}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<StatusBadge status={task.status} />
<button onClick={onOpenDetail} style={{
padding: '6px 10px', borderRadius: 8, border: '1px solid #e2e8f0',
background: detailOpen ? '#eff6ff' : '#fff',
color: detailOpen ? '#1d4ed8' : '#475569',
fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
}}>詳細</button>
</div>
</div>
);
}
function Composer({ onSend }) {
const [text, setText] = React.useState('');
const send = () => { if (!text.trim()) return; onSend(text.trim()); setText(''); };
return (
<div style={{ flexShrink: 0, borderTop: '1px solid #e2e8f0', background: '#fff', padding: 12 }}>
<div style={{
display: 'flex', alignItems: 'flex-end', gap: 8, background: '#f8fafc',
border: '1px solid #e2e8f0', borderRadius: 12, padding: 8,
}}>
<button style={{ padding: 6, background: 'transparent', border: 'none', color: '#94a3b8', cursor: 'pointer' }}>
<IconAttach width={16} height={16} />
</button>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); send(); } }}
rows={2}
placeholder="メッセージを入力 (⌘+Enter で送信)"
style={{
flex: 1, resize: 'none', border: 'none', outline: 'none', background: 'transparent',
fontFamily: 'inherit', fontSize: 13, color: '#0f172a', lineHeight: 1.5, minHeight: 32,
}}
/>
<button onClick={send} style={{
padding: '6px 14px', background: '#2563eb', color: '#fff', borderRadius: 8,
fontSize: 12, fontWeight: 700, border: 'none', cursor: text.trim() ? 'pointer' : 'not-allowed',
opacity: text.trim() ? 1 : 0.5, fontFamily: 'inherit',
}}>送信</button>
</div>
<div style={{ marginTop: 6, fontSize: 10, color: '#94a3b8', paddingLeft: 4 }}>エージェントは常に /brainstorm /plan /implement のパイプラインで動作します</div>
</div>
);
}
function ChatPane({ task, messages, onSend, onOpenDetail, detailOpen }) {
const scrollRef = React.useRef(null);
React.useEffect(() => {
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [messages.length, task.id]);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#f8fafc' }}>
<ChatHeader task={task} onOpenDetail={onOpenDetail} detailOpen={detailOpen} />
<div ref={scrollRef} style={{
flex: 1, overflowY: 'auto', padding: '16px 20px',
display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0,
}}>
{messages.map((m, i) => (
m.role === 'progress'
? <ProgressBubble key={i} text={m.content} />
: <Bubble key={i} role={m.role} footer={m.footer}>{m.content}</Bubble>
))}
</div>
<Composer onSend={onSend} />
</div>
);
}
window.ChatPane = ChatPane;
@@ -0,0 +1,134 @@
// DetailPanel — tabs: overview, progress (activity + log surface)
function Tabs({ tab, onTab }) {
const items = [
{ id: 'overview', label: '概要' },
{ id: 'progress', label: '進捗' },
{ id: 'subtasks', label: 'サブタスク' },
];
return (
<div style={{ display: 'flex', gap: 4, padding: '8px 12px', borderBottom: '1px solid #e2e8f0', background: '#fff' }}>
{items.map(it => (
<button key={it.id} onClick={() => onTab(it.id)} style={{
padding: '6px 12px', borderRadius: 8, fontSize: 12, fontWeight: 600,
border: 'none', cursor: 'pointer', fontFamily: 'inherit',
background: tab === it.id ? '#eff6ff' : 'transparent',
color: tab === it.id ? '#1d4ed8' : '#64748b',
}}>{it.label}</button>
))}
</div>
);
}
function OverviewTab({ task }) {
const Row = ({ label, value }) => (
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, padding: '8px 0', borderBottom: '1px solid #f1f5f9', fontSize: 12 }}>
<span style={{ color: '#64748b' }}>{label}</span>
<span style={{ color: '#0f172a', fontWeight: 600, textAlign: 'right' }}>{value}</span>
</div>
);
return (
<div style={{ padding: 16, overflowY: 'auto', fontSize: 13, color: '#0f172a' }}>
<div style={{ fontSize: 10, fontFamily: 'IBM Plex Mono, monospace', color: '#94a3b8', letterSpacing: '.08em' }}>DESCRIPTION</div>
<div style={{ marginTop: 6, color: '#334155', lineHeight: 1.6, fontSize: 13 }}>{task.body}</div>
<div style={{ marginTop: 16, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<StatChip label="試行" value={`${task.attempts}/3`} />
<StatChip label="ピース" value={task.piece} color="#2563eb" />
<StatChip label="ワーカー" value={task.worker || '—'} />
</div>
<div style={{ marginTop: 16 }}>
<Row label="リポジトリ" value={<span style={{ fontFamily: 'IBM Plex Mono, monospace', fontSize: 11 }}>{task.repo}</span>} />
<Row label="ブランチ" value={<span style={{ fontFamily: 'IBM Plex Mono, monospace', fontSize: 11 }}>{task.branch}</span>} />
<Row label="作成日時" value={new Date(task.createdAt).toLocaleString('ja-JP')} />
<Row label="更新日時" value={relativeTime(task.updatedAt)} />
<Row label="担当" value={task.assignee} />
</div>
<div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
<button style={{
padding: '8px 14px', borderRadius: 10, border: '1px solid #e2e8f0',
background: '#fff', color: '#475569', fontSize: 12, fontWeight: 600,
cursor: 'pointer', fontFamily: 'inherit', whiteSpace: 'nowrap',
}}>再試行</button>
<button style={{
padding: '8px 14px', borderRadius: 10, border: '1px solid #fecaca',
background: '#fff', color: '#b91c1c', fontSize: 12, fontWeight: 600,
cursor: 'pointer', fontFamily: 'inherit', whiteSpace: 'nowrap',
}}>キャンセル</button>
</div>
</div>
);
}
function ProgressTab({ task }) {
const events = task.events || [];
return (
<div style={{ padding: 16, overflowY: 'auto' }}>
<div style={{ fontSize: 10, fontFamily: 'IBM Plex Mono, monospace', color: '#94a3b8', letterSpacing: '.08em', marginBottom: 8 }}>ACTIVITY</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 20 }}>
{events.map((e, i) => (
<div key={i} style={{ display: 'flex', gap: 10, fontSize: 12 }}>
<div style={{ flexShrink: 0, marginTop: 3 }}>
<div style={{ width: 8, height: 8, borderRadius: 9999, background: e.kind === 'error' ? '#dc2626' : e.kind === 'ok' ? '#16a34a' : '#3b82f6' }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: '#0f172a', fontWeight: 600 }}>{e.label}</div>
<div style={{ color: '#64748b', fontSize: 11 }}>{e.meta}</div>
</div>
<div style={{ fontSize: 10, color: '#94a3b8', fontFamily: 'IBM Plex Mono, monospace' }}>{e.time}</div>
</div>
))}
</div>
<div style={{ fontSize: 10, fontFamily: 'IBM Plex Mono, monospace', color: '#94a3b8', letterSpacing: '.08em', marginBottom: 6 }}>ACTIVITY.LOG</div>
<div style={{
background: '#0f172a', color: '#e2e8f0',
fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, lineHeight: 1.6,
padding: 12, borderRadius: 8, whiteSpace: 'pre', overflowX: 'auto',
}}>
{`[10:42:18] ` + String.fromCharCode(9432) + ` starting worker for task #` + task.id + `
[10:42:19] ` + String.fromCharCode(9432) + ` branch: ` + task.branch + `
[10:42:21] ` + String.fromCharCode(9432) + ` piece: ` + task.piece + `
[10:42:22] ` + String.fromCharCode(9655) + ` /brainstorm
[10:43:04] ` + String.fromCharCode(10003) + ` /plan (12 steps)
[10:43:05] ` + String.fromCharCode(9655) + ` /implement
[10:44:58] ` + String.fromCharCode(10003) + ` tests passed`}
</div>
</div>
);
}
function DetailPanel({ task, onClose }) {
const [tab, setTab] = React.useState('overview');
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#f8fafc', borderLeft: '1px solid #e2e8f0' }}>
<div style={{
flexShrink: 0, padding: '12px 16px', borderBottom: '1px solid #e2e8f0', background: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8,
}}>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 10, fontFamily: 'IBM Plex Mono, monospace', color: '#94a3b8', letterSpacing: '.08em' }}>DETAIL</div>
<div style={{ fontSize: 13, fontWeight: 700, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>#{task.id} {task.title}</div>
</div>
<button onClick={onClose} style={{
width: 28, height: 28, borderRadius: 8, border: '1px solid #e2e8f0',
background: '#fff', color: '#64748b', cursor: 'pointer', display: 'inline-flex',
alignItems: 'center', justifyContent: 'center',
}}><IconClose width={12} height={12} /></button>
</div>
<Tabs tab={tab} onTab={setTab} />
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
{tab === 'overview' && <OverviewTab task={task} />}
{tab === 'progress' && <ProgressTab task={task} />}
{tab === 'subtasks' && (
<div style={{ padding: 16, fontSize: 13, color: '#64748b' }}>
サブタスクはこのタスクにありません
</div>
)}
</div>
</div>
);
}
window.DetailPanel = DetailPanel;
@@ -0,0 +1,86 @@
// Shared small primitives for the admin UI kit.
// Status tone + tiny SVG icons + labels matching the codebase.
const STATUS_LABELS = {
queued: 'Inbox', running: 'Running', waiting_human: 'Waiting',
waiting_subtasks: 'Subtasks', retry: 'Retry', succeeded: 'Done',
failed: 'Failed', cancelled: 'Cancelled',
};
const STATUS_TONE = {
running: { bg: '#dcfce7', fg: '#166534' },
waiting_human: { bg: '#fef9c3', fg: '#854d0e' },
waiting_subtasks: { bg: '#e0e7ff', fg: '#3730a3' },
failed: { bg: '#fee2e2', fg: '#b91c1c' },
succeeded: { bg: '#dbeafe', fg: '#1e40af' },
retry: { bg: '#fef3c7', fg: '#92400e' },
queued: { bg: '#e2e8f0', fg: '#475569' },
cancelled: { bg: '#e2e8f0', fg: '#475569' },
};
function StatusBadge({ status, small }) {
const tone = STATUS_TONE[status] || STATUS_TONE.queued;
const label = STATUS_LABELS[status] || status;
const style = {
background: tone.bg, color: tone.fg,
fontSize: small ? 10 : 11, fontWeight: 700,
padding: small ? '1px 8px' : '2px 10px', borderRadius: 9999,
display: 'inline-flex', alignItems: 'center', whiteSpace: 'nowrap',
};
return <span style={style}>{label}</span>;
}
function StatChip({ label, value, color }) {
return (
<div style={{
background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12,
padding: '8px 12px', boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
minWidth: 0, flex: '1 1 0', minWidth: 80,
}}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#64748b', letterSpacing: '.06em', textTransform: 'uppercase', whiteSpace: 'nowrap' }}>{label}</div>
<div style={{
fontSize: 15, fontWeight: 800, color: color || '#0f172a', marginTop: 2,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{value}</div>
</div>
);
}
function Spinner() {
return <div style={{
width: 16, height: 16, border: '2px solid #e2e8f0', borderTopColor: '#2563eb',
borderRadius: '9999px', animation: 'ao-spin 1s linear infinite', display: 'inline-block',
}} />;
}
function PulseDot() {
return <span style={{
display: 'inline-block', width: 8, height: 8, background: '#3b82f6',
borderRadius: 9999, animation: 'ao-pulse 1.2s ease-in-out infinite',
}} />;
}
function IconSearch(props) {
return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>;
}
function IconAttach(props) {
return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>;
}
function IconClose(props) {
return <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" {...props}><path d="M4 4l8 8M12 4l-8 8"/></svg>;
}
function relativeTime(ms) {
const mins = Math.floor((Date.now() - ms) / 60000);
if (mins < 1) return 'たった今';
if (mins < 60) return `${mins}分前`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}時間前`;
return `${Math.floor(hrs / 24)}日前`;
}
Object.assign(window, {
STATUS_LABELS, STATUS_TONE,
StatusBadge, StatChip, Spinner, PulseDot,
IconSearch, IconAttach, IconClose, relativeTime,
});
@@ -0,0 +1,12 @@
# Admin Dashboard UI Kit
High-fidelity recreation of the Agent Orchestrator admin UI (`ui/` in the codebase).
- `index.html` — interactive 3-column dashboard: task list, chat, detail panel. Click a task to open chat; open detail to see Overview / Progress tabs.
- `TopBar.jsx` — top navigation with wordmark, section nav, status counts, primary CTA
- `TaskList.jsx` — FilterBar + LocalTaskListItem
- `ChatPane.jsx` — header + messages + composer with user / ask / result / progress bubbles
- `DetailPanel.jsx` — tabbed detail with Overview + Progress (activity timeline + log surface)
- `Primitives.jsx` — StatusBadge, StatChip, tiny SVG icons, spinner, pulse dot
Cosmetic recreation — no real API. Data is inline sample Japanese task data.
@@ -0,0 +1,92 @@
// TaskList — FilterBar + LocalTaskListItem recreation
function FilterBar({ status, onStatus, search, onSearch, sort, onSort, counts, total }) {
const columns = ['queued', 'running', 'waiting_human', 'waiting_subtasks', 'retry', 'succeeded', 'failed', 'cancelled'];
const chipStyle = (active) => ({
flexShrink: 0, padding: '6px 10px', borderRadius: 9999,
fontSize: 11, fontWeight: 700, whiteSpace: 'nowrap', cursor: 'pointer',
border: '1px solid ' + (active ? '#2563eb' : '#e2e8f0'),
background: active ? '#eff6ff' : '#fff',
color: active ? '#1d4ed8' : '#64748b',
fontFamily: 'inherit',
});
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingBottom: 12, borderBottom: '1px solid #e2e8f0' }}>
<div style={{
display: 'flex', alignItems: 'center', gap: 8, background: '#fff', border: '1px solid #e2e8f0',
borderRadius: 12, padding: '6px 12px', boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
}}>
<IconSearch width={14} height={14} style={{ color: '#94a3b8', flexShrink: 0 }} />
<input value={search} onChange={(e) => onSearch(e.target.value)} placeholder="検索..."
style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13, fontFamily: 'inherit', color: '#0f172a', minWidth: 0 }} />
</div>
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', paddingBottom: 4 }}>
<button style={chipStyle(status === 'all')} onClick={() => onStatus('all')}>
All <span style={{ color: '#94a3b8', marginLeft: 2 }}>{total}</span>
</button>
{columns.map(s => (
<button key={s} style={chipStyle(status === s)} onClick={() => onStatus(s)}>
{STATUS_LABELS[s]} <span style={{ color: '#94a3b8', marginLeft: 2 }}>{counts[s] || 0}</span>
</button>
))}
</div>
<select value={sort} onChange={(e) => onSort(e.target.value)} style={{
padding: '6px 10px', fontSize: 12, background: '#fff', border: '1px solid #e2e8f0',
borderRadius: 8, color: '#334155', outline: 'none', fontFamily: 'inherit',
}}>
<option value="updated">新しい順</option>
<option value="status">ステータス順</option>
<option value="title">タイトル順</option>
</select>
</div>
);
}
function TaskItem({ task, active, onClick }) {
return (
<button onClick={onClick} style={{
width: '100%', textAlign: 'left', padding: '10px 12px', borderRadius: 12,
border: '1px solid ' + (active ? '#3b82f6' : '#e2e8f0'),
background: active ? '#eff6ff' : '#fff',
cursor: 'pointer', transition: 'background .15s', fontFamily: 'inherit',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
#{task.id} {task.title}
</div>
<StatusBadge status={task.status} small />
</div>
<div style={{ marginTop: 2, fontSize: 11, color: '#64748b', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{task.body.length > 60 ? task.body.slice(0, 60) + '…' : task.body}
</div>
<div style={{ marginTop: 2, fontSize: 10, color: '#94a3b8' }}>{relativeTime(task.updatedAt)}</div>
</button>
);
}
function TaskList({ tasks, activeId, onSelect, filters, setFilters }) {
const counts = {};
for (const s of ['queued', 'running', 'waiting_human', 'waiting_subtasks', 'retry', 'succeeded', 'failed', 'cancelled']) {
counts[s] = tasks.filter(t => t.status === s).length;
}
const filtered = tasks
.filter(t => filters.status === 'all' || t.status === filters.status)
.filter(t => !filters.search || (t.title + t.body).toLowerCase().includes(filters.search.toLowerCase()))
.sort((a, b) => filters.sort === 'title' ? a.title.localeCompare(b.title) : b.updatedAt - a.updatedAt);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<FilterBar
status={filters.status} onStatus={(s) => setFilters(f => ({ ...f, status: s }))}
search={filters.search} onSearch={(q) => setFilters(f => ({ ...f, search: q }))}
sort={filters.sort} onSort={(s) => setFilters(f => ({ ...f, sort: s }))}
counts={counts} total={tasks.length}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8, overflowY: 'auto', flex: 1, minHeight: 0 }}>
{filtered.map(t => <TaskItem key={t.id} task={t} active={activeId === t.id} onClick={() => onSelect(t.id)} />)}
{filtered.length === 0 && <div style={{ fontSize: 13, color: '#64748b', padding: '12px 8px' }}>スレッドがありません</div>}
</div>
</div>
);
}
window.TaskList = TaskList;
@@ -0,0 +1,62 @@
// TopBar — mirrors ui/src/components/layout/TopBar.tsx
function TopBar({ page, onNavigate, counts, onOpenCreate, user }) {
const navItem = (id, label) => (
<button
key={id}
onClick={() => onNavigate(id)}
style={{
padding: '6px 12px', borderRadius: 8, fontSize: 12, fontWeight: 500,
border: 'none', cursor: 'pointer', fontFamily: 'inherit',
background: page === id ? '#2563eb' : 'transparent',
color: page === id ? '#fff' : '#64748b',
transition: 'background .15s',
}}
>{label}</button>
);
return (
<div style={{
flexShrink: 0, background: '#fff', borderBottom: '1px solid #e2e8f0',
padding: '12px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<img src="../../assets/logo.svg" width="22" height="22" alt="" />
<span style={{
fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, fontWeight: 700,
color: '#2563eb', textTransform: 'uppercase', letterSpacing: '.16em',
}}>Agent Orchestrator</span>
<span style={{ fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, color: '#94a3b8' }}>v1.14.0</span>
<div style={{ display: 'flex', gap: 4 }}>
{navItem('tasks', 'Tasks')}
{navItem('schedules', 'Schedules')}
{navItem('settings', 'Settings')}
{navItem('users', 'Users')}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ fontSize: 11, color: '#64748b', display: 'flex', gap: 6 }}>
<span><b style={{ color: '#334155' }}>{counts.total}</b> </span>
<span><b style={{ color: '#16a34a' }}>{counts.running}</b> 実行中</span>
<span><b style={{ color: '#d97706' }}>{counts.waiting}</b> 待機</span>
{counts.failed > 0 && <span><b style={{ color: '#dc2626' }}>{counts.failed}</b> 失敗</span>}
</div>
{user && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{
width: 24, height: 24, borderRadius: 9999, background: '#dbeafe', color: '#1d4ed8',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700,
}}>{user.name.charAt(0)}</div>
<span style={{ fontSize: 12, color: '#475569' }}>{user.name}</span>
</div>
)}
<button onClick={onOpenCreate} style={{
padding: '8px 16px', background: '#2563eb', color: '#fff', borderRadius: 12,
fontSize: 13, fontWeight: 700, border: 'none', cursor: 'pointer', fontFamily: 'inherit',
}}>新しい依頼</button>
</div>
</div>
);
}
window.TopBar = TopBar;
@@ -0,0 +1,231 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Agent Orchestrator — Admin</title>
<link rel="stylesheet" href="../../colors_and_type.css">
<style>
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font-family: var(--font-sans);
background: var(--bg-app, #f8fafc);
color: var(--fg1, #0f172a);
font-size: 13px;
-webkit-font-smoothing: antialiased;
}
@keyframes ao-spin { from { transform: rotate(0) } to { transform: rotate(360deg) } }
@keyframes ao-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.35 } }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border: 2px solid #f8fafc; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/[email protected]/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script type="text/babel" src="./Primitives.jsx"></script>
<script type="text/babel" src="./TopBar.jsx"></script>
<script type="text/babel" src="./TaskList.jsx"></script>
<script type="text/babel" src="./ChatPane.jsx"></script>
<script type="text/babel" src="./DetailPanel.jsx"></script>
<script type="text/babel">
const MIN = 60 * 1000;
const H = 60 * MIN;
const now = Date.now();
const SAMPLE_TASKS = [
{
id: 412, title: 'Xの朝のAIダイジェスト生成',
body: '毎朝 7:00 JST にフォロー中のAI関連アカウントの過去24hをサマリし、DMで送信する。Twitter CLIを使用し、1スレッドにまとめること。',
status: 'running', piece: 'x-ai-digest', worker: 'worker-03', attempts: 1,
assignee: '@daichi', repo: 'gitea:daichi/agent-orchestrator', branch: 'task/412-morning-digest',
createdAt: now - 2*H, updatedAt: now - 4*MIN,
events: [
{ kind: 'info', label: '/brainstorm 完了', meta: '12個のアイデアを生成', time: '10:42' },
{ kind: 'info', label: '/plan 完了', meta: '12ステップ · 推定 4分', time: '10:43' },
{ kind: 'info', label: '/implement 実行中', meta: 'ステップ 8 / 12', time: '10:45' },
],
},
{
id: 411, title: 'Brave Search の CAPTCHA 回避',
body: 'noVNC経由でBraveに繰り返しCAPTCHAが発生。ユーザーの介入が必要。',
status: 'waiting_human', piece: 'general', worker: 'worker-01', attempts: 2,
assignee: '@daichi', repo: 'gitea:daichi/agent-orchestrator', branch: 'task/411-brave-captcha',
createdAt: now - 3*H, updatedAt: now - 18*MIN,
events: [
{ kind: 'info', label: '/brainstorm 完了', meta: '', time: '08:10' },
{ kind: 'error', label: 'ASK が発行されました', meta: 'CAPTCHAの解決を依頼', time: '08:22' },
],
},
{
id: 410, title: 'GitHub Issue #284 の対応',
body: 'scheduler.ts のタイムアウト処理リファクタ。worker-manager.test.ts を更新。',
status: 'succeeded', piece: 'general', worker: 'worker-02', attempts: 1,
assignee: '@daichi', repo: 'gitea:daichi/agent-orchestrator', branch: 'task/410-sched-timeout',
createdAt: now - 8*H, updatedAt: now - 2*H,
events: [
{ kind: 'info', label: '/plan 完了', meta: '7ステップ', time: '02:11' },
{ kind: 'ok', label: 'PR 作成', meta: '#284 テスト通過', time: '04:08' },
],
},
{
id: 409, title: 'ブレスト: 社内AIエージェント活用事例',
body: '営業部向け、週次の活用アイデアを10件ブレストし、優先順位をつけて提出。',
status: 'queued', piece: 'brainstorming', worker: null, attempts: 0,
assignee: '@tomoko', repo: 'gitea:corp/ops', branch: '—',
createdAt: now - 30*MIN, updatedAt: now - 25*MIN,
events: [],
},
{
id: 408, title: '四半期データの集計とグラフ化',
body: 'Q3のSNSエンゲージメントを集計し、CSVとPNGで出力。',
status: 'waiting_subtasks', piece: 'data-process', worker: 'worker-05', attempts: 1,
assignee: '@kenta', repo: 'gitea:corp/analytics', branch: 'task/408-q3-roundup',
createdAt: now - 5*H, updatedAt: now - 45*MIN,
events: [
{ kind: 'info', label: 'サブタスク3件を発行', meta: '#408-1, #408-2, #408-3', time: '09:30' },
],
},
{
id: 407, title: '競合サービスのリサーチ',
body: 'エージェント型ワーカー系SaaSを3社分析し、比較表を作成する。',
status: 'failed', piece: 'research', worker: 'worker-04', attempts: 3,
assignee: '@tomoko', repo: 'gitea:corp/research', branch: 'task/407-competitors',
createdAt: now - 26*H, updatedAt: now - 10*H,
events: [
{ kind: 'error', label: 'タイムアウト', meta: '3回連続で失敗', time: 'yesterday' },
],
},
{
id: 406, title: 'ゲーム実況の告知ツイート生成',
body: '今夜のストリーム用の告知ツイートを3案作成。ハッシュタグ付き。',
status: 'retry', piece: 'game-tweet-generator', worker: 'worker-06', attempts: 2,
assignee: '@daichi', repo: 'gitea:daichi/stream', branch: 'task/406-tweet-gen',
createdAt: now - 40*MIN, updatedAt: now - 8*MIN,
events: [
{ kind: 'error', label: 'レート制限', meta: '60秒後に再試行', time: '10:35' },
],
},
{
id: 405, title: '経費申請書のOCRと仕分け',
body: '添付PDFをOCRし、勘定科目ごとに仕分け。',
status: 'cancelled', piece: 'office-process', worker: null, attempts: 1,
assignee: '@kenta', repo: 'gitea:corp/ops', branch: '—',
createdAt: now - 48*H, updatedAt: now - 20*H,
events: [],
},
];
const INITIAL_MESSAGES = {
412: [
{ role: 'user', content: '毎朝7:00にAI関連のXアカウントの過去24hをまとめて、DMで送ってほしい。1スレッドで。', footer: '10:41 · @daichi' },
{ role: 'assistant', content: '了解。x-ai-digest ピースを使用します。対象アカウント、まとめる観点、文字数制限を確認させてください。' },
{ role: 'ask', content: '❓ 以下を確認させてください:\n\n1. 対象アカウントリストはこのリポジトリの accounts.txt で良いですか?\n2. 1ツイートあたりの上限文字数は280でOK?\n3. 日本語メインの要約で良いですか?' },
{ role: 'user', content: '1. OK\n2. OK\n3. 日本語で。でも原文が英語なら簡潔な英語の引用も残して。' },
{ role: 'progress', content: '/implement — ステップ 8 / 12 (Twitter CLIでタイムライン取得中)' },
],
411: [
{ role: 'user', content: 'Brave Searchで検索結果が取れない。何度もCAPTCHAが出てるっぽい。' },
{ role: 'progress', content: 'noVNCセッションを開いて確認中...' },
{ role: 'ask', content: '❓ CAPTCHAの解決が必要です。noVNCで手動で解決していただけますか?\n\nsession: https://novnc.internal/412\n\n解決後 `/resume 411` と返信してください。' },
],
410: [
{ role: 'user', content: 'Issue #284 の対応お願い。scheduler.tsのタイムアウト処理が不安定。' },
{ role: 'assistant', content: '了解。/brainstorm から始めます。' },
{ role: 'result', content: '✅ 完了しました。\n\n- PR: gitea:daichi/agent-orchestrator#291\n- 変更: scheduler.ts, worker-manager.test.ts, worker.test.ts\n- テスト: 42 passed\n\nレビューお願いします。' },
],
409: [
{ role: 'user', content: '営業部向けに今週のエージェント活用ネタを10個ブレストしてほしい。' },
{ role: 'assistant', content: '了解。キューに入りました。ワーカーの空きが出次第処理します。' },
],
408: [
{ role: 'user', content: 'Q3のSNSエンゲージメントまとめて、折れ線グラフと棒グラフのPNGで。' },
{ role: 'progress', content: 'サブタスク3件の完了を待っています (#408-1, #408-2, #408-3)' },
],
407: [
{ role: 'user', content: '競合3社のリサーチと比較表を作成。' },
{ role: 'assistant', content: '3回試行しましたが、外部サイトの読み込みタイムアウトが続いています。' },
],
406: [
{ role: 'user', content: '今夜のストリーム告知を3案、ハッシュタグ付きで。' },
{ role: 'progress', content: 'レート制限中 — 60秒後に再試行します' },
],
405: [
{ role: 'user', content: '経費PDFのOCRと仕分け。' },
{ role: 'assistant', content: 'キャンセルされました。' },
],
};
function App() {
const [tasks, setTasks] = React.useState(SAMPLE_TASKS);
const [activeId, setActiveId] = React.useState(412);
const [detailOpen, setDetailOpen] = React.useState(true);
const [messages, setMessages] = React.useState(INITIAL_MESSAGES);
const [filters, setFilters] = React.useState({ status: 'all', search: '', sort: 'updated' });
const [page, setPage] = React.useState('tasks');
const active = tasks.find(t => t.id === activeId) || tasks[0];
const counts = {
total: tasks.length,
running: tasks.filter(t => t.status === 'running').length,
waiting: tasks.filter(t => t.status === 'waiting_human' || t.status === 'waiting_subtasks').length,
failed: tasks.filter(t => t.status === 'failed').length,
};
const onSend = (text) => {
setMessages(m => ({
...m,
[activeId]: [...(m[activeId] || []), { role: 'user', content: text, footer: 'たった今 · @daichi' }],
}));
// fake echo after small delay
setTimeout(() => {
setMessages(m => ({
...m,
[activeId]: [...(m[activeId] || []), { role: 'progress', content: 'エージェントが応答を生成中...' }],
}));
}, 400);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<TopBar
page={page} onNavigate={setPage}
counts={counts}
onOpenCreate={() => alert('新しい依頼 (モック)')}
user={{ name: 'Daichi' }}
/>
<div style={{
flex: 1, minHeight: 0, display: 'grid',
gridTemplateColumns: detailOpen ? '320px 1fr 380px' : '320px 1fr',
background: '#f1f5f9', gap: 1,
}}>
<div style={{ background: '#fff', padding: 12, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
<TaskList
tasks={tasks} activeId={activeId} onSelect={setActiveId}
filters={filters} setFilters={setFilters}
/>
</div>
<ChatPane
task={active}
messages={messages[active.id] || []}
onSend={onSend}
onOpenDetail={() => setDetailOpen(v => !v)}
detailOpen={detailOpen}
/>
{detailOpen && <DetailPanel task={active} onClose={() => setDetailOpen(false)} />}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>