Open-source release of MAESTRO, an agent orchestration platform that runs LLM-driven tasks through sandboxed tools, with a web UI. Apache-2.0. See README.md and docs/ (getting-started, configuration, architecture).
400 lines
21 KiB
HTML
400 lines
21 KiB
HTML
<!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 } }
|
|
@keyframes ao-shimmer { 0% { background-position: 200% 0 } 100% { background-position: -200% 0 } }
|
|
::-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/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
|
<script src="https://unpkg.com/@babel/standalone@7.29.0/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" src="./SchedulesPage.jsx"></script>
|
|
<script type="text/babel" src="./UsersPage.jsx"></script>
|
|
<script type="text/babel" src="./SettingsPage.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: 'キャンセルされました。' },
|
|
],
|
|
};
|
|
|
|
const SAMPLE_SCHEDULES = [
|
|
{
|
|
id: 1, title: '毎朝のAIダイジェスト', body: 'フォロー中のAI関連アカウントの過去24hをサマリし、DMで送る。',
|
|
pieceName: 'x-ai-digest', outputFormat: 'markdown',
|
|
triggerKind: 'cron', cronExpression: '0 7 * * *',
|
|
nextRunAt: new Date(now + 8*H).toISOString(),
|
|
lastRunAt: new Date(now - 16*H).toISOString(),
|
|
isActive: true,
|
|
history: [
|
|
{ taskId: 412, status: 'running', summary: '実行中', at: new Date(now - 4*MIN).toISOString() },
|
|
{ taskId: 398, status: 'succeeded', summary: '12アカウント ・ 8件のハイライト', at: new Date(now - 16*H).toISOString() },
|
|
{ taskId: 385, status: 'succeeded', summary: '9アカウント ・ 5件のハイライト', at: new Date(now - 40*H).toISOString() },
|
|
{ taskId: 373, status: 'failed', summary: 'Twitter API レート制限', at: new Date(now - 64*H).toISOString() },
|
|
],
|
|
},
|
|
{
|
|
id: 2, title: '週次ニュースまとめ (月曜09:00)', body: '先週の業界ニュースを5本まとめ、社内Slackに投稿。',
|
|
pieceName: 'research', outputFormat: 'markdown',
|
|
triggerKind: 'cron', cronExpression: '0 9 * * 1',
|
|
nextRunAt: new Date(now + 4*24*H).toISOString(),
|
|
lastRunAt: new Date(now - 3*24*H).toISOString(),
|
|
isActive: true,
|
|
history: [
|
|
{ taskId: 340, status: 'succeeded', summary: '5本投稿', at: new Date(now - 3*24*H).toISOString() },
|
|
],
|
|
},
|
|
{
|
|
id: 3, title: 'GitHub Issue 自動トリアージ', body: '新規 Issue にラベル付けし、優先度を判定してコメント。',
|
|
pieceName: 'general', outputFormat: 'json',
|
|
triggerKind: 'event', eventSource: 'github.issue.opened', eventFilter: 'repo == "agent-orchestrator"',
|
|
lastRunAt: new Date(now - 2*H).toISOString(),
|
|
isActive: true,
|
|
history: [
|
|
{ taskId: 408, status: 'succeeded', summary: '#294 に bugラベルを付与', at: new Date(now - 2*H).toISOString() },
|
|
{ taskId: 402, status: 'succeeded', summary: '#293 に enhancementラベル', at: new Date(now - 6*H).toISOString() },
|
|
],
|
|
},
|
|
{
|
|
id: 4, title: '月次レポート生成', body: '月末にKPIサマリを作成し、経営会議用PDFを出力。',
|
|
pieceName: 'data-process', outputFormat: 'markdown',
|
|
triggerKind: 'cron', cronExpression: '0 18 28 * *',
|
|
nextRunAt: new Date(now + 10*24*H).toISOString(),
|
|
lastRunAt: null,
|
|
isActive: false,
|
|
history: [],
|
|
},
|
|
];
|
|
|
|
const SAMPLE_USERS = [
|
|
{ id: 'u1', name: 'Daichi', email: 'daichi@example.com', role: 'admin', status: 'active', taskCount: 142, lastLogin: 'たった今', createdAt: '2025-11-02', timezone: 'Asia/Tokyo', defaultPiece: 'auto' },
|
|
{ id: 'u2', name: 'Tomoko', email: 'tomoko@example.com', role: 'operator', status: 'active', taskCount: 38, lastLogin: '2時間前', createdAt: '2025-12-14', timezone: 'Asia/Tokyo', defaultPiece: 'chat' },
|
|
{ id: 'u3', name: 'Kenta', email: 'kenta@example.com', role: 'operator', status: 'active', taskCount: 21, lastLogin: '昨日', createdAt: '2026-01-20', timezone: 'Asia/Tokyo', defaultPiece: 'research' },
|
|
{ id: 'u4', name: 'Aya', email: 'aya@example.com', role: 'viewer', status: 'active', taskCount: 4, lastLogin: '3日前', createdAt: '2026-02-09', timezone: 'Asia/Tokyo', defaultPiece: 'auto' },
|
|
{ id: 'u5', name: null, email: 'newbie@example.com', role: 'viewer', status: 'pending', taskCount: 0, lastLogin: '—', createdAt: '2026-04-17', timezone: 'Asia/Tokyo', defaultPiece: 'auto' },
|
|
{ id: 'u6', name: 'Hiro', email: 'hiro@example.com', role: 'viewer', status: 'disabled', taskCount: 2, lastLogin: '1ヶ月前', createdAt: '2026-01-03', timezone: 'Asia/Tokyo', defaultPiece: 'auto' },
|
|
];
|
|
|
|
function DemoStateFloater({ state, setState }) {
|
|
const [open, setOpen] = React.useState(false);
|
|
const STATES = [
|
|
{ id: 'normal', label: '通常', hint: 'サンプルデータを表示' },
|
|
{ id: 'loading', label: 'Loading', hint: 'スケルトンを表示' },
|
|
{ id: 'error', label: 'Error', hint: 'エラー状態と再試行ボタン' },
|
|
{ id: 'empty', label: 'Empty', hint: 'データなし・初回利用' },
|
|
];
|
|
const current = STATES.find(s => s.id === state) || STATES[0];
|
|
return (
|
|
<div style={{
|
|
position: 'fixed', right: 16, bottom: 16, zIndex: 50,
|
|
display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8,
|
|
}}>
|
|
{open && (
|
|
<div style={{
|
|
background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12,
|
|
boxShadow: '0 10px 25px -5px rgb(0 0 0 / 0.15), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
|
padding: 8, minWidth: 220,
|
|
}}>
|
|
<div style={{
|
|
fontSize: 10, fontWeight: 700, color: '#94a3b8', letterSpacing: '.08em',
|
|
textTransform: 'uppercase', padding: '4px 10px 6px',
|
|
}}>デモ状態</div>
|
|
{STATES.map(s => (
|
|
<button key={s.id} onClick={() => setState(s.id)} style={{
|
|
display: 'block', width: '100%', textAlign: 'left', padding: '8px 10px',
|
|
border: 'none', borderRadius: 8, fontFamily: 'inherit', cursor: 'pointer',
|
|
background: state === s.id ? '#eff6ff' : 'transparent',
|
|
}}>
|
|
<div style={{ fontSize: 12, fontWeight: 700, color: state === s.id ? '#1d4ed8' : '#334155' }}>{s.label}</div>
|
|
<div style={{ fontSize: 11, color: '#64748b', marginTop: 1 }}>{s.hint}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
<button onClick={() => setOpen(v => !v)} title="デモ状態を切り替え" style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: 8,
|
|
padding: '8px 14px', borderRadius: 9999,
|
|
background: '#0f172a', color: '#fff', border: 'none', cursor: 'pointer',
|
|
fontSize: 11, fontWeight: 700, fontFamily: 'inherit',
|
|
boxShadow: '0 4px 12px rgb(0 0 0 / 0.25)',
|
|
}}>
|
|
<span style={{ width: 8, height: 8, borderRadius: 9999,
|
|
background: state === 'normal' ? '#22c55e' : state === 'loading' ? '#3b82f6' : state === 'error' ? '#ef4444' : '#94a3b8' }} />
|
|
状態: {current.label}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 [schedules, setSchedules] = React.useState(SAMPLE_SCHEDULES);
|
|
const [activeScheduleId, setActiveScheduleId] = React.useState(1);
|
|
const patchSchedule = (id, patch) => setSchedules(xs => xs.map(s => s.id === id ? { ...s, ...patch } : s));
|
|
const triggerSchedule = (id) => alert('#' + id + ' を今すぐ実行 (モック)');
|
|
const deleteSchedule = (id) => setSchedules(xs => xs.filter(s => s.id !== id));
|
|
|
|
const [users, setUsers] = React.useState(SAMPLE_USERS);
|
|
const [activeUserId, setActiveUserId] = React.useState('u1');
|
|
const patchUser = (id, patch) => setUsers(xs => xs.map(u => u.id === id ? { ...u, ...patch } : u));
|
|
const deleteUser = (id) => setUsers(xs => xs.filter(u => u.id !== id));
|
|
const approveUser = (id) => patchUser(id, { status: 'active' });
|
|
|
|
const [settingsSection, setSettingsSection] = React.useState('provider');
|
|
const [settingsPiece, setSettingsPiece] = React.useState(null);
|
|
|
|
// ── Demo state switch (loading / error / empty) — just a small floater, not part of the product ──
|
|
const [demoState, setDemoState] = React.useState(() => localStorage.getItem('admin-demo-state') || 'normal');
|
|
React.useEffect(() => { localStorage.setItem('admin-demo-state', demoState); }, [demoState]);
|
|
const isLoading = demoState === 'loading';
|
|
const hasError = demoState === 'error';
|
|
const isEmpty = demoState === 'empty';
|
|
|
|
const viewTasks = isLoading || hasError ? [] : (isEmpty ? [] : tasks);
|
|
const viewActiveId = isEmpty ? null : activeId;
|
|
const viewSchedules = isLoading || isEmpty ? [] : schedules;
|
|
const viewUsers = isLoading || isEmpty ? [] : users;
|
|
|
|
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' }}
|
|
/>
|
|
{page === 'tasks' && (
|
|
<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={viewTasks} activeId={viewActiveId} onSelect={setActiveId}
|
|
filters={filters} setFilters={setFilters}
|
|
onOpenCreate={() => alert('新しい依頼 (モック)')}
|
|
loading={isLoading}
|
|
error={hasError ? 'ネットワークエラー: 接続を確認してください' : null}
|
|
onRetry={() => setDemoState('normal')}
|
|
/>
|
|
</div>
|
|
<ChatPane
|
|
task={isLoading || hasError || isEmpty ? null : active}
|
|
messages={!active ? [] : (messages[active.id] || [])}
|
|
onSend={onSend}
|
|
onOpenDetail={() => setDetailOpen(v => !v)}
|
|
detailOpen={detailOpen}
|
|
loading={isLoading}
|
|
onOpenCreate={() => alert('新しい依頼 (モック)')}
|
|
/>
|
|
{detailOpen && !isLoading && !hasError && !isEmpty && <DetailPanel task={active} onClose={() => setDetailOpen(false)} />}
|
|
</div>
|
|
)}
|
|
{page === 'schedules' && (
|
|
<SchedulesPage
|
|
schedules={viewSchedules} activeId={isEmpty || isLoading ? null : activeScheduleId} setActiveId={setActiveScheduleId}
|
|
onPatch={patchSchedule} onTrigger={triggerSchedule} onDelete={deleteSchedule}
|
|
onOpenCreate={() => alert('新しいスケジュール (モック)')}
|
|
/>
|
|
)}
|
|
{page === 'users' && (
|
|
<UsersPage
|
|
users={viewUsers} activeId={isEmpty || isLoading ? null : activeUserId} setActiveId={setActiveUserId}
|
|
onPatch={patchUser} onDelete={deleteUser} onApprove={approveUser}
|
|
onOpenInvite={() => alert('ユーザーを招待 (モック)')}
|
|
/>
|
|
)}
|
|
{page === 'settings' && (
|
|
<SettingsPage
|
|
section={settingsSection} setSection={setSettingsSection}
|
|
piece={settingsPiece} setPiece={setSettingsPiece}
|
|
/>
|
|
)}
|
|
<DemoStateFloater state={demoState} setState={setDemoState} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
</script>
|
|
</body>
|
|
</html>
|