<!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>
