Files
openfang/crates/openfang-api/static/index_body.html
iven 92e5def702
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
初始化提交
2026-03-01 16:24:24 +08:00

4392 lines
288 KiB
HTML

<body x-data="app" :data-theme="theme">
<!-- API Key Auth Prompt -->
<div x-show="$store.app.showAuthPrompt" style="position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px)" x-data="{ apiKeyInput: '' }">
<div style="background:var(--bg-card,#1e1e2e);border:1px solid var(--border,#333);border-radius:12px;padding:2rem;max-width:400px;width:90%">
<h3 style="margin:0 0 0.5rem;font-size:1.1rem">API Key Required</h3>
<p style="color:var(--text-dim,#888);font-size:0.85rem;margin:0 0 1rem">This instance requires an API key. Enter the key from your <code>config.toml</code>.</p>
<input type="password" x-model="apiKeyInput" placeholder="Enter API key..." @keydown.enter="$store.app.submitApiKey(apiKeyInput)" style="width:100%;padding:0.6rem;border-radius:6px;border:1px solid var(--border,#333);background:var(--bg-input,#151520);color:var(--text,#e0e0e0);font-size:0.9rem;box-sizing:border-box;margin-bottom:0.75rem">
<button @click="$store.app.submitApiKey(apiKeyInput)" style="width:100%;padding:0.6rem;border-radius:6px;border:none;background:var(--accent,#7c3aed);color:#fff;font-weight:600;cursor:pointer;font-size:0.9rem">Unlock Dashboard</button>
</div>
</div>
<div class="app-layout" :class="{ 'focus-mode': $store.app.focusMode }">
<!-- Sidebar -->
<nav class="sidebar" :class="{ collapsed: sidebarCollapsed, 'mobile-open': mobileMenuOpen }">
<div class="sidebar-header">
<div class="sidebar-header-text">
<div class="sidebar-logo">
<img src="/logo.png" alt="OpenFang" width="28" height="28">
<div>
<h1>OPENFANG</h1>
<div class="version" x-text="'v' + version"></div>
</div>
</div>
</div>
<div class="theme-switcher">
<button class="theme-opt" :class="{ active: themeMode === 'light' }" @click="setTheme('light')" title="Light">&#9788;</button>
<button class="theme-opt" :class="{ active: themeMode === 'system' }" @click="setTheme('system')" title="System">&#9675;</button>
<button class="theme-opt" :class="{ active: themeMode === 'dark' }" @click="setTheme('dark')" title="Dark">&#9790;</button>
</div>
</div>
<div class="sidebar-status" :class="{ offline: !connected && !$store.app.booting }">
<span class="status-dot"></span>
<span class="sidebar-label" x-show="connected" x-text="agentCount + ' agent(s) running'"></span>
<span class="sidebar-label" x-show="$store.app.booting && !connected" class="conn-reconnecting">Connecting...</span>
<span class="sidebar-label" x-show="!connected && !$store.app.booting && $store.app.connectionState === 'reconnecting'" class="conn-reconnecting">Reconnecting...</span>
<span class="sidebar-label" x-show="!connected && !$store.app.booting && $store.app.connectionState !== 'reconnecting'" x-text="'disconnected' + ($store.app.lastError ? ' — ' + $store.app.lastError : '')">disconnected</span>
<span class="conn-badge sidebar-label" :class="wsConnected ? 'ws' : 'http'" x-text="wsConnected ? 'WS' : 'HTTP'" x-show="connected"></span>
</div>
<div class="sidebar-nav" role="navigation" aria-label="Main navigation">
<!-- Chat — primary action -->
<div class="nav-section" x-data="{ collapsed: false }" aria-label="Chat">
<div class="nav-section-title" @click="collapsed = !collapsed">
<span class="nav-label">Chat</span>
<span class="nav-section-chevron" :style="collapsed ? '' : 'transform:rotate(90deg)'">&rsaquo;</span>
</div>
<template x-if="!collapsed">
<div x-transition>
<a class="nav-item" :class="{ active: page === 'agents' }" @click="navigate('agents')" :aria-current="page === 'agents' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></span>
<span class="nav-label">Chat</span>
</a>
</div>
</template>
</div>
<!-- Monitor -->
<div class="nav-section" x-data="{ collapsed: false }" aria-label="Monitor">
<div class="nav-section-title" @click="collapsed = !collapsed">
<span class="nav-label">Monitor</span>
<span class="nav-section-chevron" :style="collapsed ? '' : 'transform:rotate(90deg)'">&rsaquo;</span>
</div>
<template x-if="!collapsed">
<div x-transition>
<a class="nav-item" :class="{ active: page === 'overview' }" @click="navigate('overview')" :aria-current="page === 'overview' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg></span>
<span class="nav-label">Overview</span>
</a>
<a class="nav-item" :class="{ active: page === 'analytics' }" @click="navigate('analytics')" :aria-current="page === 'analytics' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M18 20V10M12 20V4M6 20v-6"/></svg></span>
<span class="nav-label">Analytics</span>
</a>
<a class="nav-item" :class="{ active: page === 'logs' }" @click="navigate('logs')" :aria-current="page === 'logs' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="m4 17 6-6-6-6"/><path d="M12 19h8"/></svg></span>
<span class="nav-label">Logs</span>
</a>
</div>
</template>
</div>
<!-- Agents -->
<div class="nav-section" x-data="{ collapsed: false }" aria-label="Agents">
<div class="nav-section-title" @click="collapsed = !collapsed">
<span class="nav-label">Agents</span>
<span class="nav-section-chevron" :style="collapsed ? '' : 'transform:rotate(90deg)'">&rsaquo;</span>
</div>
<template x-if="!collapsed">
<div x-transition>
<a class="nav-item" :class="{ active: page === 'sessions' }" @click="navigate('sessions')" :aria-current="page === 'sessions' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="m12 2-10 5 10 5 10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/></svg></span>
<span class="nav-label">Sessions</span>
</a>
<a class="nav-item" :class="{ active: page === 'approvals' }" @click="navigate('approvals')" :aria-current="page === 'approvals' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg></span>
<span class="nav-label">Approvals</span>
</a>
</div>
</template>
</div>
<!-- Automation -->
<div class="nav-section" x-data="{ collapsed: false }" aria-label="Automation">
<div class="nav-section-title" @click="collapsed = !collapsed">
<span class="nav-label">Automation</span>
<span class="nav-section-chevron" :style="collapsed ? '' : 'transform:rotate(90deg)'">&rsaquo;</span>
</div>
<template x-if="!collapsed">
<div x-transition>
<a class="nav-item" :class="{ active: page === 'workflows' }" @click="navigate('workflows')" :aria-current="page === 'workflows' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M6 3v12M18 9a9 9 0 0 1-9 9"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/></svg></span>
<span class="nav-label">Workflows</span>
</a>
<a class="nav-item" :class="{ active: page === 'scheduler' }" @click="navigate('scheduler')" :aria-current="page === 'scheduler' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></span>
<span class="nav-label">Scheduler</span>
</a>
</div>
</template>
</div>
<!-- Extensions -->
<div class="nav-section" x-data="{ collapsed: false }" aria-label="Extensions">
<div class="nav-section-title" @click="collapsed = !collapsed">
<span class="nav-label">Extensions</span>
<span class="nav-section-chevron" :style="collapsed ? '' : 'transform:rotate(90deg)'">&rsaquo;</span>
</div>
<template x-if="!collapsed">
<div x-transition>
<a class="nav-item" :class="{ active: page === 'channels' }" @click="navigate('channels')" :aria-current="page === 'channels' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 9h16M4 15h16M10 3l-2 18M16 3l-2 18"/></svg></span>
<span class="nav-label">Channels</span>
</a>
<a class="nav-item" :class="{ active: page === 'skills' }" @click="navigate('skills')" :aria-current="page === 'skills' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/></svg></span>
<span class="nav-label">Skills</span>
</a>
<a class="nav-item" :class="{ active: page === 'hands' }" @click="navigate('hands')" :aria-current="page === 'hands' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M18 11V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2 2 2 0 0 0-2 2v6"/><path d="M10 10.5V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.9-5.7-2.4L3.4 16a2 2 0 0 1 3.2-2.4L8 15"/></svg></span>
<span class="nav-label">Hands</span>
</a>
</div>
</template>
</div>
<!-- System -->
<div class="nav-section" x-data="{ collapsed: false }" aria-label="System">
<div class="nav-section-title" @click="collapsed = !collapsed">
<span class="nav-label">System</span>
<span class="nav-section-chevron" :style="collapsed ? '' : 'transform:rotate(90deg)'">&rsaquo;</span>
</div>
<template x-if="!collapsed">
<div x-transition>
<a class="nav-item" :class="{ active: page === 'settings' }" @click="navigate('settings')" :aria-current="page === 'settings' ? 'page' : false">
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/></svg></span>
<span class="nav-label">Settings</span>
</a>
</div>
</template>
</div>
</div>
<div class="sidebar-footer">
<div class="sidebar-label text-xs text-dim" style="padding:0 16px 4px;letter-spacing:0.5px">Ctrl+K agents | Ctrl+N new</div>
</div>
<div class="sidebar-toggle" @click="toggleSidebar()" x-text="sidebarCollapsed ? '\u276F' : '\u276E'"></div>
</nav>
<div class="sidebar-overlay" @click="mobileMenuOpen = false"></div>
<!-- Main Content -->
<main class="main-content">
<!-- Mobile menu button -->
<button class="mobile-menu-btn btn btn-ghost" @click="mobileMenuOpen = !mobileMenuOpen" style="position:fixed;top:8px;left:8px;z-index:98;padding:6px 10px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
<!-- Page: Overview -->
<template x-if="page === 'overview'">
<div x-data="overviewPage" x-init="loadOverview().then(() => startAutoRefresh())" @page-leave.window="stopAutoRefresh()">
<div class="page-header">
<h2>Overview</h2>
<div class="flex items-center gap-2">
<span class="health-indicator" :class="health.status === 'ok' ? 'health-ok' : 'health-down'" x-text="health.status === 'ok' ? 'Healthy' : 'Unreachable'"></span>
<button class="btn btn-ghost btn-sm" @click="silentRefresh()" title="Refresh" style="padding:4px 8px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
</button>
</div>
</div>
<div class="page-body">
<!-- Loading skeleton state -->
<div x-show="loading" style="animation:fadeIn 0.2s">
<div class="stats-row stats-row-lg" style="margin-bottom:20px">
<div class="stat-card stat-card-lg"><div class="skeleton skeleton-heading" style="width:60px;height:28px"></div><div class="skeleton skeleton-text" style="width:100px;height:12px;margin-top:8px"></div></div>
<div class="stat-card stat-card-lg"><div class="skeleton skeleton-heading" style="width:40px;height:28px"></div><div class="skeleton skeleton-text" style="width:120px;height:12px;margin-top:8px"></div></div>
<div class="stat-card stat-card-lg"><div class="skeleton skeleton-heading" style="width:50px;height:28px"></div><div class="skeleton skeleton-text" style="width:80px;height:12px;margin-top:8px"></div></div>
<div class="stat-card stat-card-lg"><div class="skeleton skeleton-heading" style="width:40px;height:28px"></div><div class="skeleton skeleton-text" style="width:60px;height:12px;margin-top:8px"></div></div>
</div>
<div class="card" style="margin-bottom:16px"><div class="skeleton skeleton-text" style="width:120px;margin-bottom:12px"></div><div style="display:flex;gap:8px"><div class="skeleton" style="width:80px;height:24px;border-radius:20px"></div><div class="skeleton" style="width:70px;height:24px;border-radius:20px"></div><div class="skeleton" style="width:90px;height:24px;border-radius:20px"></div></div></div>
<div class="overview-grid"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>
</div>
<!-- Error state -->
<div x-show="!loading && loadError" class="error-state" style="animation:fadeIn 0.3s">
<div class="empty-state-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--error)" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
</div>
<h3 style="color:var(--error)">Connection Error</h3>
<p class="text-xs text-dim" x-text="loadError"></p>
<button class="btn btn-primary btn-sm" @click="loadData()" style="margin-top:8px">Retry</button>
</div>
<!-- Setup Checklist -->
<div class="setup-checklist" x-show="!loading && !loadError && setupProgress < 100 && !checklistDismissed" style="animation:slideUp 0.3s var(--ease-spring)">
<div class="card" style="border-left:4px solid var(--accent)">
<div class="flex justify-between items-center mb-2">
<div>
<div class="card-header" style="margin:0">Getting Started</div>
<div class="text-xs text-dim" x-text="setupDoneCount + ' of 5 steps completed'"></div>
</div>
<div class="flex gap-2">
<button class="btn btn-primary btn-sm" @click="location.hash='wizard'">Setup Wizard</button>
<button class="btn btn-ghost btn-sm" @click="dismissChecklist()">Dismiss</button>
</div>
</div>
<div class="progress-bar mb-2" style="margin-top:8px">
<div class="progress-bar-fill" :style="'width:' + setupProgress + '%'"></div>
</div>
<template x-for="item in setupChecklist" :key="item.key">
<div class="setup-checklist-item">
<div class="setup-checklist-icon" :class="{ done: item.done }">
<span x-show="item.done">&#10003;</span>
<span x-show="!item.done">&#9675;</span>
</div>
<span style="flex:1" :style="item.done ? 'text-decoration:line-through;opacity:0.6' : ''" x-text="item.label"></span>
<a x-show="!item.done" :href="item.action" class="btn btn-ghost btn-sm" style="font-size:10px;padding:3px 8px">Go</a>
</div>
</template>
</div>
</div>
<!-- Onboarding banner -->
<div class="onboarding-banner" x-show="!loading && !loadError && $store.app.showOnboarding && checklistDismissed">
<h3>Welcome to OpenFang</h3>
<p style="font-size:12px;color:var(--text-dim)">Get started quickly with the guided Setup Wizard, or configure manually:</p>
<div class="flex gap-2">
<button class="btn btn-primary" @click="location.hash='wizard'">Launch Setup Wizard</button>
<button class="btn btn-ghost" @click="location.hash='settings'">Configure Manually</button>
<button class="btn btn-ghost" @click="$store.app.dismissOnboarding()">Dismiss</button>
</div>
</div>
<div x-show="!loading && !loadError" style="animation:fadeIn 0.3s">
<!-- Primary stats row with icons + stagger animation -->
<div class="stats-row stats-row-lg">
<div class="stat-card stat-card-lg animate-entry stagger-1" @click="location.hash='agents'" style="cursor:pointer">
<div style="display:flex;align-items:center;gap:8px">
<div style="width:36px;height:36px;border-radius:var(--radius-md);background:var(--accent-subtle);display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<div>
<div class="stat-value" :class="(status.agent_count || 0) > 0 ? 'stat-value-success' : ''" x-text="status.agent_count || 0"></div>
<div class="stat-label">Agents Running</div>
</div>
</div>
</div>
<div class="stat-card stat-card-lg animate-entry stagger-2">
<div style="display:flex;align-items:center;gap:8px">
<div style="width:36px;height:36px;border-radius:var(--radius-md);background:rgba(96,165,250,0.1);display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#60A5FA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 2-10 5 10 5 10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/></svg>
</div>
<div>
<div class="stat-value" x-text="formatNumber(usageSummary.total_tokens)"></div>
<div class="stat-label">Tokens Used</div>
</div>
</div>
</div>
<div class="stat-card stat-card-lg animate-entry stagger-3">
<div style="display:flex;align-items:center;gap:8px">
<div style="width:36px;height:36px;border-radius:var(--radius-md);background:rgba(74,222,128,0.1);display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#4ADE80" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</div>
<div>
<div class="stat-value stat-value-accent" x-text="formatCost(usageSummary.total_cost)"></div>
<div class="stat-label">Total Cost</div>
</div>
</div>
</div>
<div class="stat-card stat-card-lg animate-entry stagger-4">
<div style="display:flex;align-items:center;gap:8px">
<div style="width:36px;height:36px;border-radius:var(--radius-md);background:rgba(168,85,247,0.1);display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#A855F7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<div>
<div class="stat-value" x-text="formatUptime(status.uptime_seconds)"></div>
<div class="stat-label">Uptime</div>
</div>
</div>
</div>
</div>
<!-- Secondary stats row -->
<div class="stats-row animate-entry stagger-5" style="margin-bottom:16px">
<div class="stat-card" @click="location.hash='channels'" style="cursor:pointer">
<div class="stat-value" style="font-size:18px" x-text="channels.length"></div>
<div class="stat-label">Channels</div>
</div>
<div class="stat-card" @click="location.hash='skills'" style="cursor:pointer">
<div class="stat-value" style="font-size:18px" x-text="skillCount"></div>
<div class="stat-label">Skills</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:18px" x-text="connectedMcp.length"></div>
<div class="stat-label">MCP Servers</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:18px" x-text="formatNumber(usageSummary.total_tools)"></div>
<div class="stat-label">Tool Calls</div>
</div>
<div class="stat-card">
<div class="stat-value" :class="configuredProviders.length > 0 ? 'stat-value-success' : ''" style="font-size:18px" x-text="configuredProviders.length"></div>
<div class="stat-label">Providers</div>
</div>
</div>
<!-- Provider status badges with health indicators -->
<div class="card mb-4" x-show="providers.length" style="overflow:hidden">
<div class="flex justify-between items-center mb-2">
<div class="card-header" style="margin:0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;margin-right:4px;vertical-align:-2px"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
LLM Providers
</div>
<span class="text-xs text-dim" x-text="configuredProviders.length + '/' + providers.length + ' configured'"></span>
</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">
<template x-for="p in providers" :key="p.id">
<div class="badge" :class="providerBadgeClass(p)" :title="providerTooltip(p)" style="cursor:pointer;transition:all 0.15s var(--ease-spring);padding:4px 10px" @click="location.hash='settings'" @mouseenter="$el.style.transform='translateY(-1px)'" @mouseleave="$el.style.transform=''">
<span x-show="p.auth_status === 'configured'" style="display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px" :style="p.health === 'cooldown' || p.health === 'open' ? 'background:var(--warning);animation:pulse-ring 1.5s infinite' : 'background:var(--success)'"></span>
<span x-text="p.display_name"></span>
</div>
</template>
</div>
</div>
<div class="overview-grid">
<!-- System Health -->
<div class="card">
<div class="card-header">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;margin-right:4px;vertical-align:-2px"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
System Health
</div>
<div class="detail-grid" style="margin-top:8px">
<div class="detail-row"><span class="detail-label">Status</span><span class="badge" :class="health.status === 'ok' ? 'badge-running' : 'badge-crashed'" x-text="health.status || 'unknown'"></span></div>
<div class="detail-row"><span class="detail-label">Version</span><span class="detail-value font-mono" x-text="status.version || '-'"></span></div>
<div class="detail-row"><span class="detail-label">Provider</span><span class="detail-value" x-text="status.default_provider || '-'"></span></div>
<div class="detail-row"><span class="detail-label">Model</span><span class="detail-value font-mono" style="font-size:11px" x-text="status.default_model || '-'"></span></div>
</div>
</div>
<!-- Security Systems -->
<div class="card">
<div class="card-header">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;margin-right:4px;vertical-align:-2px"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Security Systems
</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-top:8px">
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">Merkle Audit</span>
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">Taint Tracking</span>
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">WASM Sandbox</span>
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">GCRA Rate Limit</span>
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">Ed25519 Signing</span>
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">SSRF Protection</span>
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">Secret Zeroize</span>
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">Loop Guard</span>
<span class="badge badge-success" style="font-size:9px;padding:2px 8px">Session Repair</span>
</div>
<div class="text-xs text-dim" style="margin-top:8px">9 defense-in-depth systems active</div>
</div>
<!-- Connected Channels -->
<div class="card" x-show="channels.length">
<div class="card-header">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;margin-right:4px;vertical-align:-2px"><path d="M4 9h16M4 15h16M10 3l-2 18M16 3l-2 18"/></svg>
Connected Channels
</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-top:8px">
<template x-for="ch in channels" :key="ch.name">
<span class="badge badge-info" style="font-size:9px;text-transform:capitalize;padding:2px 8px" x-text="ch.name"></span>
</template>
</div>
<div class="text-xs text-dim" style="margin-top:8px" x-text="channels.length + ' channel(s) connected'"></div>
</div>
<!-- MCP Servers -->
<div class="card" x-show="mcpServers.length">
<div class="card-header">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;margin-right:4px;vertical-align:-2px"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><path d="M6 6h.01M6 18h.01"/></svg>
MCP Servers
</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-top:8px">
<template x-for="s in mcpServers" :key="s.name">
<div class="badge" :class="s.status === 'connected' ? 'badge-success' : 'badge-dim'" style="font-size:9px;padding:2px 8px">
<span style="display:inline-block;width:5px;height:5px;border-radius:50%;margin-right:3px" :style="s.status === 'connected' ? 'background:var(--success)' : 'background:var(--text-dim)'"></span>
<span x-text="s.name"></span>
<span class="text-xs text-dim" x-show="s.tool_count" x-text="'(' + s.tool_count + ' tools)'"></span>
</div>
</template>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card mt-4" style="background:var(--bg-elevated)">
<div class="card-header" style="margin-bottom:8px">Quick Actions</div>
<div style="display:flex;flex-wrap:wrap;gap:8px">
<button class="btn btn-ghost btn-sm" @click="location.hash='agents'" style="display:flex;align-items:center;gap:4px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>
New Agent
</button>
<button class="btn btn-ghost btn-sm" @click="location.hash='skills'" style="display:flex;align-items:center;gap:4px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/></svg>
Browse Skills
</button>
<button class="btn btn-ghost btn-sm" @click="location.hash='channels'" style="display:flex;align-items:center;gap:4px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 9h16M4 15h16M10 3l-2 18M16 3l-2 18"/></svg>
Add Channel
</button>
<button class="btn btn-ghost btn-sm" @click="location.hash='workflows'" style="display:flex;align-items:center;gap:4px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3v12M18 9a9 9 0 0 1-9 9"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/></svg>
Create Workflow
</button>
<button class="btn btn-ghost btn-sm" @click="location.hash='settings'" style="display:flex;align-items:center;gap:4px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/></svg>
Settings
</button>
</div>
</div>
<!-- Recent Activity Feed -->
<div class="card mt-4" x-show="recentAudit.length">
<div class="flex justify-between items-center">
<div class="card-header" style="margin:0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;margin-right:4px;vertical-align:-2px"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
Recent Activity
</div>
<a class="btn btn-ghost btn-sm" @click="location.hash='logs'" style="font-size:10px;padding:2px 8px">View All</a>
</div>
<div style="margin-top:12px;display:flex;flex-direction:column;gap:1px">
<template x-for="(e, idx) in recentAudit" :key="e.seq">
<div class="detail-row" style="padding:8px 0;border-bottom:1px solid var(--border-subtle);animation:slideUp 0.2s var(--ease-spring)" :style="'animation-delay:' + (idx * 30) + 'ms'">
<div style="display:flex;align-items:center;gap:8px;flex:1;min-width:0">
<div style="width:24px;height:24px;border-radius:var(--radius-sm);background:var(--surface-2);display:flex;align-items:center;justify-content:center;flex-shrink:0;color:var(--text-dim)" x-html="actionIcon(e.action)"></div>
<div style="min-width:0;flex:1">
<div style="display:flex;align-items:center;gap:6px">
<span class="badge" :class="actionBadgeClass(e.action)" style="font-size:9px;padding:1px 6px" x-text="friendlyAction(e.action)"></span>
<span class="text-xs text-dim truncate" style="max-width:100px" x-text="agentName(e.agent_id)" :title="e.agent_id"></span>
</div>
<div class="text-xs text-dim truncate" style="margin-top:2px;max-width:300px" x-show="e.detail" x-text="e.detail" :title="e.detail"></div>
</div>
</div>
<span class="text-xs text-dim font-mono" style="white-space:nowrap;flex-shrink:0" x-text="timeAgo(e.timestamp)" :title="new Date(e.timestamp).toLocaleString()"></span>
</div>
</template>
</div>
</div>
<!-- Empty activity state -->
<div class="card mt-4" x-show="!recentAudit.length && !loading">
<div style="text-align:center;padding:24px 16px">
<div class="empty-state-icon" style="margin:0 auto 8px">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<h3 style="color:var(--text-secondary);margin-bottom:4px">No Recent Activity</h3>
<p class="text-xs text-dim">Activity will appear here once agents start processing.</p>
<button class="btn btn-primary btn-sm" @click="location.hash='agents'" style="margin-top:12px">Chat with an Agent</button>
</div>
</div>
<!-- Quick Actions -->
<div class="animate-entry stagger-6" style="display:grid;grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));gap:12px;margin-top:20px">
<div class="quick-action-card" @click="location.hash='agents'">
<div class="quick-action-icon" style="background:var(--accent-subtle)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
</div>
<div>
<div class="quick-action-label">Create Agent</div>
<div class="quick-action-desc">Spawn a new agent</div>
</div>
</div>
<div class="quick-action-card" @click="location.hash='settings'">
<div class="quick-action-icon" style="background:var(--success-subtle)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
</div>
<div>
<div class="quick-action-label">Configure Provider</div>
<div class="quick-action-desc">Set up an LLM provider</div>
</div>
</div>
<div class="quick-action-card" @click="location.hash='skills'">
<div class="quick-action-icon" style="background:var(--info-subtle)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--info)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/></svg>
</div>
<div>
<div class="quick-action-label">Browse Skills</div>
<div class="quick-action-desc">Explore available skills</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Page: Agents -->
<template x-if="page === 'agents'">
<div x-data="agentsPage" @close-chat.window="closeChat()">
<!-- MODE 1: Inline Chat (agent selected) -->
<template x-if="activeChatAgent">
<div x-data="chatPage" class="chat-wrapper">
<!-- Chat header with back button -->
<div class="page-header" x-show="currentAgent">
<div class="flex items-center gap-2" style="min-width:0">
<button class="btn btn-ghost btn-sm" @click="$dispatch('close-chat')" title="Back to Agents" style="padding:4px 8px;margin-right:4px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<span x-show="currentAgent && currentAgent.identity && currentAgent.identity.emoji" x-text="currentAgent && currentAgent.identity && currentAgent.identity.emoji" style="font-size:20px;line-height:1"></span>
<div style="min-width:0">
<div class="font-bold" x-text="currentAgent ? currentAgent.name : ''"></div>
<div class="text-xs text-dim truncate" style="font-family:var(--font-mono);font-size:11px" x-text="currentAgent ? currentAgent.model_provider + ':' + currentAgent.model_name : ''"></div>
</div>
</div>
<div class="flex gap-2 items-center">
<span class="badge badge-running" x-show="currentAgent && !sending" style="animation:fadeIn 0.2s">
<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--success);margin-right:2px"></span> Ready
</span>
<span class="badge badge-suspended" x-show="sending" style="animation:fadeIn 0.2s">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5" style="animation:spin 1s linear infinite"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
Generating...
</span>
<span class="queue-badge" x-show="messageQueue.length > 0" x-text="'+' + messageQueue.length + ' queued'"></span>
<!-- Session switcher -->
<div style="position:relative">
<button class="btn btn-ghost btn-sm" @click="sessionsOpen = !sessionsOpen" title="Sessions" style="position:relative">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>
<span class="session-count-badge" x-show="sessions.length > 1" x-text="sessions.length"></span>
</button>
<div class="session-dropdown" x-show="sessionsOpen" @click.outside="sessionsOpen = false" x-transition>
<div class="session-dropdown-header">
<span class="text-xs font-bold">Sessions</span>
<button class="btn btn-ghost btn-sm" @click="createSession()" style="padding:2px 6px;font-size:11px">+ New</button>
</div>
<template x-for="s in sessions" :key="s.session_id">
<div class="session-item" :class="{ active: s.active }" @click="if (!s.active) { switchSession(s.session_id); sessionsOpen = false; }">
<span class="session-dot" :class="s.active ? 'active' : ''"></span>
<div style="flex:1;min-width:0">
<div class="text-xs font-bold truncate" x-text="s.label || 'Session ' + s.session_id.substring(0, 8)"></div>
<div class="text-xs text-dim" x-text="s.message_count + ' messages'"></div>
</div>
</div>
</template>
<div class="text-xs text-dim" style="padding:8px 12px;text-align:center" x-show="!sessions.length">No sessions</div>
</div>
</div>
<button class="btn btn-ghost btn-sm" @click="toggleSearch()" title="Search messages (Ctrl+F)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
</button>
<button class="btn btn-ghost btn-sm" @click="$store.app.toggleFocusMode()" title="Ctrl+Shift+F">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><template x-if="!$store.app.focusMode"><g><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></g></template><template x-if="$store.app.focusMode"><g><path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/></g></template></svg>
</button>
<button class="btn btn-danger btn-sm" @click="killAgent()">Stop</button>
</div>
</div>
<!-- Search bar -->
<div class="chat-search-bar" x-show="searchOpen" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0 transform -translate-y-2" x-transition:enter-end="opacity-100 transform translate-y-0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="text" id="chat-search-input" x-model="searchQuery" x-ref="searchInput" placeholder="Search messages..." class="chat-search-input" @keydown.escape="toggleSearch()">
<span class="text-xs text-dim" x-show="searchQuery.trim()" x-text="filteredMessages.length + ' of ' + messages.length"></span>
<button class="btn btn-ghost btn-sm" @click="toggleSearch()" style="padding:2px 6px">&times;</button>
</div>
<!-- Messages area -->
<div class="messages" id="messages" @dragover.prevent="dragOver = true" @dragleave="dragOver = false" @drop.prevent="handleDrop($event); dragOver = false">
<!-- Message list -->
<template x-if="currentAgent">
<div>
<template x-for="(msg, $index) in filteredMessages" :key="msg.id">
<div class="message" :class="msg.role + (msg.thinking ? ' thinking' : '') + (msg.streaming ? ' streaming' : '') + (isGrouped($index) ? ' grouped' : '')">
<div class="message-avatar" x-show="msg.role === 'agent'">
<img src="/logo.png" alt="OpenFang">
</div>
<div class="message-body">
<!-- Copy button on hover -->
<div class="message-actions" x-show="msg.text && msg.text.trim() && !msg.thinking && !msg.streaming">
<button class="message-action-btn" :class="{ copied: msg._copied }" @click="copyMessage(msg)" :title="msg._copied ? 'Copied!' : 'Copy message'">
<svg x-show="!msg._copied" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
<svg x-show="msg._copied" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
</button>
</div>
<!-- Thinking indicator with animated dots -->
<div class="message-bubble" x-show="msg.thinking" style="background:transparent;border:none;padding:4px 0">
<div class="typing-dots"><span></span><span></span><span></span></div>
</div>
<!-- Normal message content -->
<div class="message-bubble" :class="{ 'markdown-body': (msg.role === 'agent' || msg.role === 'system') && !msg.thinking && !msg.isHtml }" x-show="msg.text && msg.text.trim() && !msg.thinking" x-html="highlightSearch(msg.isHtml ? msg.text : ((msg.role === 'agent' || msg.role === 'system') && !msg.thinking ? renderMarkdown(msg.text) : escapeHtml(msg.text)))"></div>
<!-- Inline images from uploads (shown above tool cards) -->
<template x-if="msg.images && msg.images.length">
<div style="display:flex;flex-wrap:wrap;gap:8px;margin:8px 0">
<template x-for="img in msg.images" :key="img.file_id">
<a :href="'/api/uploads/' + img.file_id" target="_blank" style="display:block">
<img :src="'/api/uploads/' + img.file_id" :alt="img.filename || 'uploaded image'" style="max-width:320px;max-height:320px;border-radius:8px;border:1px solid var(--border);cursor:pointer" loading="lazy">
</a>
</template>
</div>
</template>
<template x-for="tool in (msg.tools || [])" :key="tool.id">
<div class="tool-card" :class="{ 'tool-card-error': tool.is_error }" :data-tool="tool.name">
<div class="tool-card-header" @click="tool.expanded = !tool.expanded">
<template x-if="tool.running"><div class="tool-card-spinner"></div></template>
<template x-if="!tool.running && !tool.is_error"><span class="tool-icon-ok">&#10003;</span></template>
<template x-if="!tool.running && tool.is_error"><span class="tool-icon-err">&#10007;</span></template>
<span class="tool-card-icon" x-html="toolIcon(tool.name)"></span>
<span class="tool-card-name" x-text="tool.name"></span>
<span class="text-xs text-dim" x-text="tool.running ? 'running...' : (tool.is_error ? 'error' : (tool.result ? (tool.result.length > 500 ? Math.round(tool.result.length/1024) + 'KB' : 'done') : 'done'))" style="margin-left:auto"></span>
<span class="tool-expand-chevron" :style="tool.expanded ? 'transform:rotate(90deg)' : ''" style="transition:transform 0.15s">&#9654;</span>
</div>
<!-- Render generated images above the raw result -->
<div x-show="tool._imageUrls && tool._imageUrls.length" style="padding:8px 12px;display:flex;flex-wrap:wrap;gap:8px">
<template x-for="iurl in (tool._imageUrls || [])" :key="iurl">
<a :href="iurl" target="_blank" style="display:block">
<img :src="iurl" alt="Generated image" style="max-width:320px;max-height:320px;border-radius:8px;border:1px solid var(--border);cursor:pointer" loading="lazy">
</a>
</template>
</div>
<!-- Audio player for TTS results -->
<div x-show="tool._audioFile" style="padding:8px 12px">
<div class="audio-player">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>
<span class="text-xs" x-text="'Audio: ' + tool._audioFile.split('/').pop()"></span>
<span class="text-xs text-dim" x-show="tool._audioDuration" x-text="'~' + Math.round((tool._audioDuration || 0) / 1000) + 's'"></span>
</div>
</div>
<div class="tool-card-body" x-show="tool.expanded" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
<div x-show="tool.input" style="margin-bottom:6px">
<div class="tool-section-label">Input</div>
<pre class="tool-pre" x-text="formatToolJson(tool.input)"></pre>
</div>
<div x-show="tool.result">
<div class="tool-section-label">Result <span class="text-xs text-muted" x-show="tool.result && tool.result.length > 200" x-text="'(' + tool.result.length + ' chars)'"></span></div>
<pre class="tool-pre" :class="{ 'tool-pre-error': tool.is_error, 'tool-pre-short': !tool.is_error && tool.result && tool.result.length < 100, 'tool-pre-medium': !tool.is_error && tool.result && tool.result.length >= 100 && tool.result.length < 500 }" x-text="formatToolJson(tool.result)"></pre>
</div>
</div>
</div>
</template>
<!-- Timestamp + meta row -->
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<div class="message-time" x-text="formatTime(msg.ts)" x-show="msg.ts && !msg.thinking"></div>
<div class="message-meta" x-text="msg.meta" x-show="msg.meta"></div>
</div>
</div>
</div>
</template>
</div>
</template>
<!-- Drop zone overlay -->
<div x-show="dragOver" style="position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:var(--surface);opacity:0.92;border:2px dashed var(--accent);border-radius:8px;pointer-events:none">
<span style="font-size:16px;color:var(--accent);font-weight:600">Drop files here</span>
</div>
</div>
<!-- Input area -->
<div class="input-area" x-show="currentAgent" style="position:relative">
<!-- Attachment previews -->
<div x-show="attachments.length > 0" style="display:flex;gap:8px;flex-wrap:wrap;padding:0 0 8px 0">
<template x-for="(att, aidx) in attachments" :key="aidx">
<div style="position:relative;border:1px solid var(--border);border-radius:6px;padding:4px;display:flex;align-items:center;gap:6px;background:var(--surface2);max-width:180px">
<img x-show="att.preview" :src="att.preview" style="width:32px;height:32px;object-fit:cover;border-radius:4px">
<span x-show="!att.preview" style="font-size:18px;width:32px;text-align:center">&#128196;</span>
<span class="text-xs truncate" style="max-width:100px" x-text="att.file.name"></span>
<span x-show="att.uploading" class="spinner" style="width:12px;height:12px;border-width:2px"></span>
<button @click="removeAttachment(aidx)" style="position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;background:var(--danger);color:#fff;border:none;cursor:pointer;font-size:11px;display:flex;align-items:center;justify-content:center;line-height:1">&times;</button>
</div>
</template>
</div>
<!-- Slash command menu -->
<div x-show="showSlashMenu && filteredSlashCommands.length" class="slash-menu">
<template x-for="(cmd, idx) in filteredSlashCommands" :key="cmd.cmd">
<div class="slash-menu-item" :class="{ 'slash-active': idx === slashIdx }" @click="executeSlashCommand(cmd.cmd)" @mouseenter="slashIdx = idx">
<span class="font-bold" style="font-size:13px" x-text="cmd.cmd"></span>
<span class="text-xs text-dim" x-text="cmd.desc"></span>
</div>
</template>
</div>
<!-- Input row -->
<div class="input-row">
<button class="btn btn-ghost btn-sm" @click="$refs.fileInput.click()" title="Attach file" style="padding:6px 8px;flex-shrink:0">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
</button>
<input type="file" x-ref="fileInput" multiple accept="image/*,.txt,.pdf,.md,.json,.csv,.mp3,.wav,.ogg,.webm,.m4a,.flac" @change="addFiles($event.target.files); $event.target.value = ''" style="display:none">
<!-- Mic button -->
<button class="btn btn-ghost btn-sm" :class="{ 'btn-recording': recording }" @mousedown="startRecording()" @mouseup="stopRecording()" @mouseleave="if(recording) stopRecording()" title="Hold to record voice" style="padding:6px 8px;flex-shrink:0">
<svg x-show="!recording" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
<span x-show="recording" class="recording-dot"></span>
</button>
<!-- Recording indicator -->
<div class="recording-indicator" x-show="recording" x-transition>
<span class="recording-dot"></span>
<span class="text-xs" style="color:var(--danger)" x-text="formatRecordingTime()"></span>
</div>
<textarea id="msg-input" rows="1" :placeholder="recording ? 'Recording... release to send' : 'Message OpenFang... (/ for commands)'"
@keydown.enter.prevent="if(!$event.shiftKey){if(showSlashMenu && filteredSlashCommands.length){executeSlashCommand(filteredSlashCommands[slashIdx].cmd)}else{sendMessage()}}"
@keydown.escape="showSlashMenu = false"
@keydown.arrow-up.prevent="if(showSlashMenu){slashIdx = Math.max(0, slashIdx - 1)}"
@keydown.arrow-down.prevent="if(showSlashMenu){slashIdx = Math.min(filteredSlashCommands.length - 1, slashIdx + 1)}"
@input="$el.style.height='auto';$el.style.height=Math.min($el.scrollHeight,150)+'px'"
x-model="inputText"
:class="{ 'streaming-active': sending }"></textarea>
<!-- Send button (normal) or Stop button (during streaming) -->
<template x-if="!sending">
<button class="btn-send" @click="sendMessage()" :disabled="!inputText.trim() && !attachments.length">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
</button>
</template>
<template x-if="sending">
<button class="btn-stop" @click="stopAgent()" title="Stop generating">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
</button>
</template>
</div>
<!-- Footer: tokens + queue + tips -->
<div class="input-footer">
<div class="flex items-center gap-2">
<span class="text-xs text-dim" x-text="tokenCount > 0 ? '~' + tokenCount + ' tokens' : (attachments.length ? attachments.length + ' file(s)' : '')"></span>
<span class="queue-badge" x-show="messageQueue.length > 0" x-text="messageQueue.length + ' queued'"></span>
</div>
<div class="tip-bar" x-show="currentTip && !sending">
<span class="text-xs" x-text="currentTip"></span>
<button class="tip-bar-dismiss" @click="dismissTips()" title="Dismiss">&times;</button>
</div>
</div>
</div>
</div>
</template>
<!-- MODE 2: Agent picker + Templates (no agent chatting) -->
<div x-show="!activeChatAgent" class="chat-wrapper">
<div class="page-header">
<h2>Chat</h2>
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm" x-show="agents.length" @click="openSpawnWizard()">+ New Agent</button>
<button class="btn btn-danger btn-sm" x-show="agents.length > 1" @click="killAllAgents()">Stop All</button>
</div>
</div>
<div class="page-body" style="overflow-y:auto;padding:24px">
<!-- Running agents — click to chat -->
<div x-show="agents.length" style="margin-bottom:28px">
<div class="text-sm font-bold mb-2" style="color:var(--text-dim);letter-spacing:0.5px;font-size:11px;text-transform:uppercase">Your Agents</div>
<div style="display:flex;flex-wrap:wrap;gap:10px">
<template x-for="agent in agents" :key="agent.id">
<div class="agent-chip" @click="chatWithAgent(agent)" style="cursor:pointer;display:flex;align-items:center;gap:10px;padding:12px 18px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);transition:all 0.2s var(--ease-spring, ease);min-width:200px;box-shadow:var(--shadow-xs)" @mouseenter="$el.style.borderColor='var(--accent)';$el.style.transform='translateY(-2px)';$el.style.boxShadow='var(--shadow-md)'" @mouseleave="$el.style.borderColor='var(--border)';$el.style.transform='';$el.style.boxShadow='var(--shadow-xs)'">
<div style="width:36px;height:36px;border-radius:50%;background:var(--accent-subtle);display:flex;align-items:center;justify-content:center;flex-shrink:0">
<span x-show="agent.identity && agent.identity.emoji" x-text="agent.identity && agent.identity.emoji" style="font-size:18px"></span>
<svg x-show="!agent.identity || !agent.identity.emoji" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<div style="min-width:0;flex:1">
<div class="font-bold" style="font-size:13px" x-text="agent.name"></div>
<div class="text-xs text-dim font-mono" style="font-size:11px" x-text="agent.model_name"></div>
</div>
<span class="badge" :class="'badge-' + agent.state.toLowerCase()" x-text="agent.state" style="font-size:10px"></span>
</div>
</template>
</div>
</div>
<!-- Quick-start templates — spawn + chat in one click -->
<div>
<div class="text-sm font-bold mb-2" style="color:var(--text-dim);letter-spacing:0.5px;font-size:11px;text-transform:uppercase" x-text="agents.length ? 'Or Start a New Agent' : 'Start Chatting'"></div>
<div class="card-grid">
<template x-for="t in builtinTemplates" :key="t.name">
<div class="card" style="cursor:pointer" @click="spawnBuiltin(t)">
<div class="flex justify-between items-center mb-1">
<div class="card-header" style="margin:0;font-size:14px;font-weight:600" x-text="t.name"></div>
<span class="badge badge-dim" x-text="t.category"></span>
</div>
<div class="text-sm" style="line-height:1.5;color:var(--text-dim)" x-text="t.description"></div>
</div>
</template>
</div>
</div>
<!-- Custom spawn modal still available -->
</div>
<!-- Agent detail modal with tabs (Info / Files / Config) -->
<template x-if="showDetailModal && detailAgent">
<div class="modal-overlay" @click.self="showDetailModal = false" @keydown.escape.window="showDetailModal = false">
<div class="modal" style="max-width:600px">
<div class="modal-header">
<h3>
<span x-show="detailAgent.identity && detailAgent.identity.emoji" x-text="detailAgent.identity && detailAgent.identity.emoji" style="margin-right:6px"></span>
<span x-text="detailAgent.name"></span>
</h3>
<button class="modal-close" @click="showDetailModal = false">&times;</button>
</div>
<div class="tabs" style="margin-bottom:16px">
<div class="tab" :class="{ active: detailTab === 'info' }" @click="detailTab = 'info'">Info</div>
<div class="tab" :class="{ active: detailTab === 'files' }" @click="detailTab = 'files'; loadAgentFiles()">Files</div>
<div class="tab" :class="{ active: detailTab === 'config' }" @click="detailTab = 'config'">Config</div>
</div>
<!-- Tab: Info -->
<div x-show="detailTab === 'info'">
<div class="detail-grid">
<div class="detail-row"><span class="detail-label">ID</span><span class="detail-value text-xs" x-text="detailAgent.id" style="word-break:break-all"></span></div>
<div class="detail-row"><span class="detail-label">State</span><span class="badge" :class="'badge-' + detailAgent.state.toLowerCase()" x-text="detailAgent.state"></span></div>
<div class="detail-row"><span class="detail-label">Mode</span>
<select class="form-select" style="width:140px" :value="detailAgent.mode || 'full'" @change="setMode(detailAgent, $event.target.value)">
<option value="observe">Observe</option><option value="assist">Assist</option><option value="full">Full</option>
</select>
</div>
<div class="detail-row" x-show="detailAgent.profile"><span class="detail-label">Profile</span><span class="detail-value" style="text-transform:capitalize" x-text="detailAgent.profile || '-'"></span></div>
<div class="detail-row"><span class="detail-label">Provider</span><span class="detail-value" x-text="detailAgent.model_provider"></span></div>
<div class="detail-row"><span class="detail-label">Model</span><span class="detail-value" x-text="detailAgent.model_name"></span></div>
<div class="detail-row"><span class="detail-label">Created</span><span class="detail-value" x-text="detailAgent.created_at ? new Date(detailAgent.created_at).toLocaleString() : '-'"></span></div>
</div>
<div class="flex gap-2 mt-4">
<button class="btn btn-primary" @click="chatWithAgent(detailAgent); showDetailModal = false">Chat</button>
<button class="btn btn-ghost" @click="cloneAgent(detailAgent)">Clone</button>
<button class="btn btn-danger" @click="killAgent(detailAgent)">Stop</button>
</div>
</div>
<!-- Tab: Files -->
<div x-show="detailTab === 'files'">
<div x-show="filesLoading" class="text-center text-dim" style="padding:24px">Loading files...</div>
<div x-show="!filesLoading && !editingFile">
<template x-for="file in agentFiles" :key="file.name">
<div class="file-list-item" @click="openFile(file)">
<span class="font-mono" style="font-size:13px" x-text="file.name"></span>
<span class="text-xs text-dim" x-text="file.exists ? (file.size_bytes + ' bytes') : 'Not created'"></span>
</div>
</template>
<div x-show="!agentFiles.length && !filesLoading" class="text-center text-dim" style="padding:24px">No workspace files found</div>
</div>
<div x-show="editingFile">
<div class="flex justify-between items-center mb-2">
<span class="font-bold font-mono" x-text="editingFile"></span>
<button class="btn btn-ghost btn-sm" @click="closeFileEditor()">&larr; Back</button>
</div>
<textarea class="file-editor" x-model="fileContent" style="width:100%;height:280px;resize:vertical"></textarea>
<div class="flex gap-2 mt-2">
<button class="btn btn-primary btn-sm" @click="saveFile()" :disabled="fileSaving"><span x-show="!fileSaving">Save</span><span x-show="fileSaving">Saving...</span></button>
<button class="btn btn-ghost btn-sm" @click="closeFileEditor()">Cancel</button>
</div>
</div>
</div>
<!-- Tab: Config (inline editable) -->
<div x-show="detailTab === 'config'">
<div class="form-group"><label>Name</label><input class="form-input" x-model="configForm.name"></div>
<div class="form-group"><label>System Prompt</label><textarea class="form-textarea" x-model="configForm.system_prompt" style="height:80px"></textarea></div>
<div class="form-group"><label>Emoji</label>
<div class="emoji-grid">
<template x-for="em in emojiOptions" :key="em">
<button class="emoji-grid-item" :class="{ active: configForm.emoji === em }" @click="configForm.emoji = em" x-text="em"></button>
</template>
</div>
</div>
<div class="form-group"><label>Color</label><input type="color" x-model="configForm.color" style="width:48px;height:32px;border:none;cursor:pointer;background:none"></div>
<div class="form-group"><label>Archetype</label>
<select class="form-select" x-model="configForm.archetype">
<option value="">None</option>
<template x-for="a in archetypeOptions" :key="a"><option :value="a.toLowerCase()" x-text="a"></option></template>
</select>
</div>
<div class="form-group"><label>Vibe</label>
<select class="form-select" x-model="configForm.vibe">
<option value="">None</option>
<option value="professional">Professional</option><option value="friendly">Friendly</option>
<option value="technical">Technical</option><option value="creative">Creative</option>
<option value="concise">Concise</option><option value="mentor">Mentor</option>
</select>
</div>
<button class="btn btn-primary mt-4" @click="saveConfig()" :disabled="configSaving">
<span x-show="!configSaving">Save Config</span><span x-show="configSaving">Saving...</span>
</button>
</div>
</div>
</div>
</template>
<!-- Multi-step Spawn Wizard -->
<template x-if="showSpawnModal">
<div class="modal-overlay" @click.self="showSpawnModal = false" @keydown.escape.window="showSpawnModal = false">
<div class="modal" style="max-width:560px">
<div class="modal-header">
<h3>Create Agent</h3>
<button class="modal-close" @click="showSpawnModal = false">&times;</button>
</div>
<!-- Wizard / TOML toggle -->
<div class="tabs" style="margin-bottom:12px">
<div class="tab" :class="{ active: spawnMode === 'wizard' }" @click="spawnMode = 'wizard'">Wizard</div>
<div class="tab" :class="{ active: spawnMode === 'toml' }" @click="spawnMode = 'toml'">Raw TOML</div>
</div>
<!-- Raw TOML mode (unchanged) -->
<div x-show="spawnMode === 'toml'">
<div class="form-group">
<label>Agent Manifest (TOML)</label>
<textarea class="form-textarea" style="height:200px;font-size:11px" x-model="spawnToml" placeholder='name = "my-agent"&#10;module = "builtin:chat"&#10;[model]&#10;provider = "groq"&#10;model = "llama-3.3-70b-versatile"&#10;system_prompt = "You are helpful."'></textarea>
</div>
<button class="btn btn-primary btn-block mt-4" @click="spawnAgent()" :disabled="spawning">
<span x-show="!spawning">Spawn Agent</span><span x-show="spawning">Spawning...</span>
</button>
</div>
<!-- Wizard steps -->
<div x-show="spawnMode === 'wizard'">
<!-- Step indicator -->
<div class="wizard-steps">
<template x-for="s in [1,2,3,4,5]" :key="s">
<div class="wizard-dot" :class="{ active: spawnStep === s, done: spawnStep > s }">
<span x-text="spawnStep > s ? '\u2713' : s"></span>
</div>
</template>
</div>
<!-- Step 1: Name + Identity -->
<div x-show="spawnStep === 1">
<div class="form-group">
<label>Agent Name</label>
<input class="form-input" x-model="spawnForm.name" placeholder="my-agent" @keydown.enter="nextStep()">
</div>
<div class="form-group">
<label>Emoji</label>
<div class="emoji-grid">
<template x-for="em in emojiOptions" :key="em">
<button class="emoji-grid-item" :class="{ active: spawnIdentity.emoji === em }" @click="spawnIdentity.emoji = em" x-text="em"></button>
</template>
</div>
</div>
<div class="form-group">
<label>Color</label>
<input type="color" x-model="spawnIdentity.color" style="width:48px;height:32px;border:none;cursor:pointer;background:none">
</div>
<div class="form-group">
<label>Archetype</label>
<select class="form-select" x-model="spawnIdentity.archetype">
<option value="">Choose...</option>
<template x-for="a in archetypeOptions" :key="a"><option :value="a.toLowerCase()" x-text="a"></option></template>
</select>
</div>
</div>
<!-- Step 2: Model Selection -->
<div x-show="spawnStep === 2">
<div class="form-group">
<label>Provider</label>
<select class="form-select" x-model="spawnForm.provider">
<option value="anthropic">Anthropic</option><option value="openai">OpenAI</option>
<option value="groq">Groq</option><option value="ollama">Ollama</option>
<option value="google">Google</option><option value="mistral">Mistral</option>
<option value="xai">xAI</option><option value="deepseek">DeepSeek</option>
<option value="cerebras">Cerebras</option><option value="sambanova">SambaNova</option>
<option value="together">Together</option>
</select>
</div>
<div class="form-group">
<label>Model</label>
<input class="form-input" x-model="spawnForm.model" placeholder="e.g. llama-3.3-70b-versatile">
</div>
<div class="form-group">
<label>System Prompt</label>
<textarea class="form-textarea" x-model="spawnForm.systemPrompt" placeholder="You are a helpful assistant."></textarea>
</div>
</div>
<!-- Step 3: Personality Presets -->
<div x-show="spawnStep === 3">
<label class="mb-2" style="display:block;font-size:13px;font-weight:600">Personality</label>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px">
<template x-for="preset in personalityPresets" :key="preset.id">
<button class="personality-pill" :class="{ active: selectedPreset === preset.id }" @click="selectPreset(preset)" x-text="preset.label"></button>
</template>
</div>
<div class="form-group">
<label>Soul / Persona <span class="text-xs text-dim">(editable)</span></label>
<textarea class="form-textarea" x-model="soulContent" style="height:100px" placeholder="Describe this agent's personality and communication style..."></textarea>
</div>
</div>
<!-- Step 4: Tools & Capabilities -->
<div x-show="spawnStep === 4">
<div class="form-group">
<label>Tool Profile</label>
<select class="form-select" x-model="spawnForm.profile" @focus="loadSpawnProfiles()">
<option value="minimal">Minimal &mdash; Read-only file access</option>
<option value="coding">Coding &mdash; Files + shell + web fetch</option>
<option value="research">Research &mdash; Web search + file read/write</option>
<option value="messaging">Messaging &mdash; Agents + memory access</option>
<option value="automation">Automation &mdash; All tools except custom</option>
<option value="full" selected>Full &mdash; All 35+ tools</option>
<option value="custom">Custom (manual capabilities)</option>
</select>
<div class="capability-preview" x-show="selectedProfileTools.length > 0" style="margin-top:8px">
<div class="capability-preview-title">Tools included</div>
<div style="display:flex;flex-wrap:wrap;gap:2px">
<template x-for="tool in selectedProfileTools" :key="tool"><span class="tool-badge" x-text="tool"></span></template>
<span class="tool-badge" x-show="selectedProfileTools.length >= 15" style="opacity:0.6">...</span>
</div>
</div>
</div>
<div class="form-group" x-show="spawnForm.profile === 'custom'">
<label>Capabilities</label>
<div class="flex gap-3" style="flex-wrap:wrap">
<label class="form-checkbox"><input type="checkbox" x-model="spawnForm.caps.memory_read"> Memory Read</label>
<label class="form-checkbox"><input type="checkbox" x-model="spawnForm.caps.memory_write"> Memory Write</label>
<label class="form-checkbox"><input type="checkbox" x-model="spawnForm.caps.network"> Network</label>
<label class="form-checkbox"><input type="checkbox" x-model="spawnForm.caps.shell"> Shell</label>
<label class="form-checkbox"><input type="checkbox" x-model="spawnForm.caps.agent_spawn"> Agent Spawn</label>
</div>
</div>
</div>
<!-- Step 5: Review & Spawn -->
<div x-show="spawnStep === 5">
<div class="card" style="margin-bottom:16px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
<div style="width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:22px" :style="'background:' + spawnIdentity.color + '22; border:2px solid ' + spawnIdentity.color">
<span x-text="spawnIdentity.emoji || '\u{1F916}'"></span>
</div>
<div>
<div class="font-bold" style="font-size:15px" x-text="spawnForm.name || 'Unnamed'"></div>
<div class="text-xs text-dim" x-text="spawnIdentity.archetype || 'agent'"></div>
</div>
</div>
<div class="detail-grid" style="gap:6px">
<div class="detail-row"><span class="detail-label">Provider</span><span class="detail-value" x-text="spawnForm.provider"></span></div>
<div class="detail-row"><span class="detail-label">Model</span><span class="detail-value" x-text="spawnForm.model"></span></div>
<div class="detail-row"><span class="detail-label">Profile</span><span class="detail-value" x-text="spawnForm.profile"></span></div>
<div class="detail-row" x-show="selectedPreset"><span class="detail-label">Personality</span><span class="detail-value" style="text-transform:capitalize" x-text="selectedPreset"></span></div>
</div>
</div>
</div>
<!-- Navigation buttons -->
<div class="flex justify-between mt-4">
<button class="btn btn-ghost" x-show="spawnStep > 1" @click="prevStep()">Back</button>
<div x-show="spawnStep <= 1"></div>
<button class="btn btn-primary" x-show="spawnStep < 5" @click="nextStep()">Next</button>
<button class="btn btn-primary" x-show="spawnStep === 5" @click="spawnAgent()" :disabled="spawning">
<span x-show="!spawning">Spawn Agent</span><span x-show="spawning">Spawning...</span>
</button>
</div>
</div>
</div>
</div>
</template>
</div><!-- /activeChatAgent picker wrapper -->
</div>
</template>
<!-- Page: Approvals -->
<template x-if="page === 'approvals'">
<div x-data="approvalsPage" x-init="loadData()">
<div class="page-header">
<h2>Execution Approvals</h2>
<div class="flex items-center gap-2">
<span class="badge badge-warn" x-show="pendingCount > 0" x-text="pendingCount + ' pending'"></span>
<button class="btn btn-ghost btn-sm" @click="loadData()">Refresh</button>
</div>
</div>
<div class="page-body">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<template x-if="!loading && !loadError">
<div>
<div class="filter-pills mb-4">
<button class="filter-pill" :class="{ active: filterStatus === 'all' }" @click="filterStatus = 'all'">All</button>
<button class="filter-pill" :class="{ active: filterStatus === 'pending' }" @click="filterStatus = 'pending'">Pending</button>
<button class="filter-pill" :class="{ active: filterStatus === 'approved' }" @click="filterStatus = 'approved'">Approved</button>
<button class="filter-pill" :class="{ active: filterStatus === 'rejected' }" @click="filterStatus = 'rejected'">Rejected</button>
</div>
<div x-show="filtered.length === 0" class="empty-state">
<h4>No approvals</h4>
<p class="hint">When agents request permission for sensitive actions, they'll appear here.</p>
</div>
<div class="card-grid">
<template x-for="a in filtered" :key="a.id">
<div class="card approval-card" :class="{ approved: a.status === 'approved', rejected: a.status === 'rejected', expired: a.status === 'expired' }">
<div class="flex justify-between items-center mb-2">
<span class="card-header" style="margin:0" x-text="a.action"></span>
<span class="badge" :class="{ 'badge-warn': a.status === 'pending', 'badge-success': a.status === 'approved', 'badge-error': a.status === 'rejected', 'badge-muted': a.status === 'expired' }" x-text="a.status"></span>
</div>
<div class="text-sm text-dim mb-2" x-text="a.description"></div>
<div class="text-xs text-dim">Agent: <span x-text="a.agent_name"></span> &middot; <span x-text="timeAgo(a.created_at)"></span></div>
<template x-if="a.status === 'pending'">
<div class="approval-actions" style="display:flex;gap:8px;margin-top:12px">
<button class="btn btn-success btn-sm" @click="approve(a.id)">Approve</button>
<button class="btn btn-danger btn-sm" @click="reject(a.id)">Reject</button>
</div>
</template>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Page: Workflows -->
<template x-if="page === 'workflows'">
<div x-data="{ wfTab: 'list' }">
<div class="page-header">
<h2>Workflows</h2>
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm" :class="{ active: wfTab === 'list' }" @click="wfTab = 'list'">List</button>
<button class="btn btn-ghost btn-sm" :class="{ active: wfTab === 'builder' }" @click="wfTab = 'builder'">Visual Builder</button>
</div>
</div>
<!-- Tab: List -->
<template x-if="wfTab === 'list'">
<div x-data="workflowsPage">
<div class="page-body" x-init="loadWorkflows()">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading workflows...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<div class="card mb-4" style="border-left:3px solid var(--accent)">
<div class="font-bold" style="font-size:13px;margin-bottom:4px">What are Workflows?</div>
<div class="text-sm text-dim" style="line-height:1.6">
Workflows chain multiple agents into automated pipelines. Each step runs an agent with a prompt template,
passing output from one step as input to the next. Steps can run sequentially, fan out in parallel, loop, or branch conditionally.
<br><span style="margin-top:4px;display:inline-block">Try the <strong style="color:var(--accent);cursor:pointer" @click="$dispatch('wf-switch-tab','builder')">Visual Builder</strong> to drag and drop workflow steps.</span>
</div>
</div>
<div class="flex gap-2 mb-4">
<button class="btn btn-primary btn-sm" @click="showCreateModal = true">+ New Workflow</button>
</div>
<div class="table-wrap" x-show="workflows.length">
<table>
<thead><tr><th>Name</th><th>Steps</th><th>Created</th><th>Actions</th></tr></thead>
<tbody>
<template x-for="wf in workflows" :key="wf.id">
<tr>
<td><span class="font-bold" x-text="wf.name"></span><br><span class="text-xs text-dim" x-text="wf.description"></span></td>
<td x-text="Array.isArray(wf.steps) ? wf.steps.length + ' step' + (wf.steps.length !== 1 ? 's' : '') : wf.steps"></td>
<td class="text-xs" x-text="new Date(wf.created_at).toLocaleDateString()"></td>
<td>
<button class="btn btn-primary btn-sm" @click="showRunModal(wf)">Run</button>
<button class="btn btn-ghost btn-sm" @click="viewRuns(wf)">History</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!workflows.length">
<div class="empty-state-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3v12M18 9a9 9 0 0 1-9 9"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/></svg>
</div>
<h3>No workflows yet</h3>
<p>Chain multiple agents into automated pipelines with branching, fan-out, and loops.</p>
<button class="btn btn-primary" @click="showCreateModal = true">Create Workflow</button>
</div>
</div>
<!-- Create modal -->
<template x-if="showCreateModal">
<div class="modal-overlay" @click.self="showCreateModal = false" @keydown.escape.window="showCreateModal = false">
<div class="modal">
<div class="modal-header"><h3>Create Workflow</h3><button class="modal-close" @click="showCreateModal = false">&times;</button></div>
<div class="form-group"><label>Name</label><input class="form-input" x-model="newWf.name" placeholder="my-workflow"></div>
<div class="form-group"><label>Description</label><input class="form-input" x-model="newWf.description" placeholder="What does this workflow do?"></div>
<div class="mb-4">
<div class="form-group" style="margin:0"><label>Steps</label></div>
<div class="text-xs text-dim mb-2">Each step runs an agent. Use <code style="color:var(--accent)">{{input}}</code> in prompts to pass the previous step's output.</div>
<template x-for="(step, i) in newWf.steps" :key="i">
<div class="card mt-2" style="padding:10px">
<div class="flex gap-2 items-center">
<span class="text-xs text-dim font-bold" x-text="'#' + (i+1)" style="width:24px"></span>
<input class="form-input" style="flex:1" x-model="step.name" placeholder="Step name">
<input class="form-input" style="flex:1" x-model="step.agent_name" placeholder="Agent name">
<select class="form-select" style="width:120px" x-model="step.mode">
<option value="sequential">Sequential</option>
<option value="fan_out">Fan Out</option>
<option value="conditional">Conditional</option>
<option value="loop">Loop</option>
</select>
<button class="btn btn-danger btn-sm" @click="newWf.steps.splice(i,1)">&times;</button>
</div>
<input class="form-input mt-2" x-model="step.prompt" placeholder="Prompt template (use {{input}})">
</div>
</template>
<button class="btn btn-ghost btn-sm mt-2" @click="newWf.steps.push({name:'',agent_name:'',mode:'sequential',prompt:'{{input}}'})">+ Add Step</button>
</div>
<button class="btn btn-primary btn-block" @click="createWorkflow()">Create</button>
</div>
</div>
</template>
<!-- Run modal -->
<template x-if="runModal">
<div class="modal-overlay" @click.self="runModal = null" @keydown.escape.window="runModal = null">
<div class="modal">
<div class="modal-header"><h3 x-text="'Run: ' + runModal.name"></h3><button class="modal-close" @click="runModal = null">&times;</button></div>
<div class="form-group"><label>Input</label><textarea class="form-textarea" x-model="runInput" placeholder="Enter workflow input..."></textarea></div>
<button class="btn btn-primary btn-block" @click="executeWorkflow()" :disabled="running">
<span x-show="!running">Execute</span><span x-show="running">Running...</span>
</button>
<div class="card mt-4" x-show="runResult">
<div class="card-header">Result</div>
<pre style="font-size:11px;white-space:pre-wrap;margin-top:8px;color:var(--text-dim)" x-text="runResult"></pre>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Tab: Visual Builder -->
<template x-if="wfTab === 'builder'">
<div x-data="workflowBuilder()" x-init="init()">
<div class="wf-builder-layout">
<!-- Node palette (left sidebar) -->
<div class="wf-palette">
<div class="wf-palette-title">Node Palette</div>
<div class="text-xs text-dim mb-3">Drag nodes onto the canvas</div>
<template x-for="nt in nodeTypes" :key="nt.type">
<div class="wf-palette-node" draggable="true" @dragstart="onPaletteDragStart(nt.type, $event)"
:style="'border-left: 3px solid ' + nt.color">
<span class="wf-palette-icon" :style="'background:' + nt.color" x-text="nt.icon"></span>
<span class="text-xs" x-text="nt.label"></span>
</div>
</template>
<hr style="border-color:var(--border);margin:12px 0">
<div class="text-xs text-dim mb-2">Workflow</div>
<div class="form-group" style="margin-bottom:8px">
<input class="form-input" x-model="workflowName" placeholder="Workflow name" style="font-size:11px">
</div>
<div class="form-group" style="margin-bottom:8px">
<input class="form-input" x-model="workflowDescription" placeholder="Description" style="font-size:11px">
</div>
<div class="flex flex-col gap-1">
<button class="btn btn-primary btn-sm btn-block" @click="generateToml()">Export TOML</button>
<button class="btn btn-primary btn-sm btn-block" @click="showSaveModal = true">Save Workflow</button>
<button class="btn btn-ghost btn-sm btn-block" @click="autoLayout()">Auto Layout</button>
<button class="btn btn-ghost btn-sm btn-block" @click="clearCanvas()">Clear</button>
</div>
</div>
<!-- Canvas -->
<div class="wf-canvas-wrap">
<!-- Zoom controls -->
<div class="wf-zoom-controls">
<button class="btn btn-ghost btn-sm" @click="zoomOut()" title="Zoom out">-</button>
<span class="text-xs" x-text="Math.round(zoom * 100) + '%'" style="min-width:36px;text-align:center"></span>
<button class="btn btn-ghost btn-sm" @click="zoomIn()" title="Zoom in">+</button>
<button class="btn btn-ghost btn-sm" @click="zoomReset()" title="Reset view">Fit</button>
</div>
<svg id="wf-canvas" class="wf-canvas"
@mousedown="onCanvasMouseDown($event)"
@mousemove="onCanvasMouseMove($event)"
@mouseup="onCanvasMouseUp()"
@mouseleave="onCanvasMouseUp()"
@wheel.prevent="onCanvasWheel($event)"
@drop="onCanvasDrop($event)"
@dragover="onCanvasDragOver($event)">
<g :transform="'translate(' + (canvasOffset.x * zoom) + ',' + (canvasOffset.y * zoom) + ') scale(' + zoom + ')'">
<!-- Grid pattern -->
<defs>
<pattern id="wf-grid" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="var(--text-dim)" opacity="0.15"/>
</pattern>
</defs>
<rect x="-2000" y="-2000" width="6000" height="6000" fill="url(#wf-grid)"/>
<!-- Connections -->
<template x-for="conn in connections" :key="conn.id">
<g>
<path :d="getConnectionPath(conn)" fill="none" stroke="var(--text-dim)" stroke-width="2"
style="cursor:pointer" @click.stop="selectedConnection = conn"
:stroke="selectedConnection && selectedConnection.id === conn.id ? 'var(--accent)' : 'var(--text-dim)'"
:stroke-width="selectedConnection && selectedConnection.id === conn.id ? 3 : 2"/>
<!-- Arrow at midpoint -->
</g>
</template>
<!-- Connection preview line -->
<template x-if="connecting && connectPreview">
<path :d="getPreviewPath()" fill="none" stroke="var(--accent)" stroke-width="2" stroke-dasharray="6,3"/>
</template>
<!-- Nodes -->
<template x-for="node in nodes" :key="node.id">
<g class="wf-node" :transform="'translate(' + node.x + ',' + node.y + ')'"
@mousedown="onNodeMouseDown(node, $event)" @dblclick="editNode(node)">
<!-- Node body -->
<rect x="0" y="0" :width="node.width" :height="node.height" rx="8" ry="8"
:fill="selectedNode && selectedNode.id === node.id ? 'var(--card-bg)' : 'var(--bg-secondary)'"
:stroke="selectedNode && selectedNode.id === node.id ? node.color : 'var(--border)'"
stroke-width="2" style="cursor:grab"/>
<!-- Color accent bar -->
<rect x="0" y="0" width="6" :height="node.height" rx="3" ry="0" :fill="node.color"/>
<!-- Icon -->
<circle cx="28" :cy="node.height/2" r="14" :fill="node.color" opacity="0.15"/>
<text x="28" :y="node.height/2 + 4" text-anchor="middle" :fill="node.color"
style="font-size:12px;font-weight:700;pointer-events:none" x-text="node.icon"></text>
<!-- Label -->
<text x="50" :y="node.height/2 - 4" fill="var(--text)" style="font-size:12px;font-weight:600;pointer-events:none"
x-text="node.label"></text>
<!-- Sub-label (config hint) -->
<text x="50" :y="node.height/2 + 12" fill="var(--text-dim)" style="font-size:10px;pointer-events:none"
x-text="node.type === 'agent' ? (node.config.agent_name || 'No agent') :
node.type === 'condition' ? (node.config.expression || 'No condition') :
node.type === 'loop' ? ('max ' + (node.config.max_iterations || 5) + ' iters') :
node.type === 'parallel' ? (node.config.fan_count + ' branches') :
node.type === 'collect' ? node.config.strategy : ''"></text>
<!-- Input ports -->
<template x-for="pi in Array.from({length: node.ports.in}, function(_,i){return i})" :key="'in-'+pi">
<circle class="wf-port wf-port-in" :cx="node.width / (node.ports.in + 1) * (pi + 1)" cy="0" r="6"
fill="var(--bg-secondary)" stroke="var(--text-dim)" stroke-width="2"
@mouseup.stop="endConnect(node.id, pi, $event)"/>
</template>
<!-- Output ports -->
<template x-for="po in Array.from({length: node.ports.out}, function(_,i){return i})" :key="'out-'+po">
<circle class="wf-port wf-port-out" :cx="node.width / (node.ports.out + 1) * (po + 1)" :cy="node.height" r="6"
fill="var(--bg-secondary)" :stroke="node.color" stroke-width="2"
@mousedown.stop="startConnect(node.id, po, $event)"/>
</template>
</g>
</template>
</g>
</svg>
<!-- Canvas hints -->
<div class="wf-canvas-hint" x-show="nodes.length <= 1">
Drag nodes from the palette onto the canvas. Connect output ports (bottom) to input ports (top). Double-click to edit.
</div>
</div>
<!-- Node editor panel (right sidebar) -->
<div class="wf-editor-panel" x-show="showNodeEditor && selectedNode" x-transition>
<div class="wf-editor-header">
<span class="font-bold text-sm" x-text="selectedNode ? selectedNode.label : ''"></span>
<button class="btn btn-ghost btn-sm" @click="showNodeEditor = false">&times;</button>
</div>
<template x-if="selectedNode">
<div>
<div class="form-group">
<label class="text-xs">Label</label>
<input class="form-input" x-model="selectedNode.label" style="font-size:11px">
</div>
<!-- Agent config -->
<template x-if="selectedNode.type === 'agent'">
<div>
<div class="form-group">
<label class="text-xs">Agent</label>
<select class="form-select" x-model="selectedNode.config.agent_name" style="font-size:11px">
<option value="">Select agent...</option>
<template x-for="a in agents" :key="a.id || a.name">
<option :value="a.name" x-text="a.name"></option>
</template>
</select>
</div>
<div class="form-group">
<label class="text-xs">Prompt Template</label>
<textarea class="form-textarea" x-model="selectedNode.config.prompt" style="font-size:11px;min-height:60px" placeholder="{{input}}"></textarea>
</div>
<div class="form-group">
<label class="text-xs">Model (optional)</label>
<input class="form-input" x-model="selectedNode.config.model" style="font-size:11px" placeholder="Default model">
</div>
</div>
</template>
<!-- Condition config -->
<template x-if="selectedNode.type === 'condition'">
<div>
<div class="form-group">
<label class="text-xs">Expression</label>
<input class="form-input" x-model="selectedNode.config.expression" style="font-size:11px" placeholder="output.contains('yes')">
</div>
<div class="text-xs text-dim">Top port = true, bottom port = false</div>
</div>
</template>
<!-- Loop config -->
<template x-if="selectedNode.type === 'loop'">
<div>
<div class="form-group">
<label class="text-xs">Max Iterations</label>
<input type="number" class="form-input" x-model.number="selectedNode.config.max_iterations" style="font-size:11px" min="1" max="100">
</div>
<div class="form-group">
<label class="text-xs">Until (stop condition)</label>
<input class="form-input" x-model="selectedNode.config.until" style="font-size:11px" placeholder="output === 'done'">
</div>
</div>
</template>
<!-- Parallel config -->
<template x-if="selectedNode.type === 'parallel'">
<div>
<div class="form-group">
<label class="text-xs">Fan-out Count</label>
<input type="number" class="form-input" x-model.number="selectedNode.config.fan_count" style="font-size:11px" min="2" max="10">
</div>
</div>
</template>
<!-- Collect config -->
<template x-if="selectedNode.type === 'collect'">
<div>
<div class="form-group">
<label class="text-xs">Strategy</label>
<select class="form-select" x-model="selectedNode.config.strategy" style="font-size:11px">
<option value="all">Wait for all</option>
<option value="first">First to finish</option>
<option value="majority">Majority vote</option>
</select>
</div>
</div>
</template>
<hr style="border-color:var(--border);margin:12px 0">
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm" @click="duplicateNode(selectedNode)">Duplicate</button>
<button class="btn btn-danger btn-sm" @click="deleteNode(selectedNode.id)">Delete</button>
</div>
</div>
</template>
</div>
</div>
<!-- Delete connection hint -->
<div class="wf-conn-hint" x-show="selectedConnection" x-transition>
<span class="text-xs">Connection selected</span>
<button class="btn btn-danger btn-sm" @click="deleteConnection(selectedConnection.id)">Delete Connection</button>
</div>
<!-- TOML Preview modal -->
<template x-if="showTomlPreview">
<div class="modal-overlay" @click.self="showTomlPreview = false" @keydown.escape.window="showTomlPreview = false">
<div class="modal" style="max-width:600px">
<div class="modal-header"><h3>Generated TOML</h3><button class="modal-close" @click="showTomlPreview = false">&times;</button></div>
<pre class="wf-toml-preview" x-text="tomlOutput"></pre>
<button class="btn btn-ghost btn-block mt-2" @click="navigator.clipboard.writeText(tomlOutput); OpenFangToast.success('Copied!')">Copy to Clipboard</button>
</div>
</div>
</template>
<!-- Save modal -->
<template x-if="showSaveModal">
<div class="modal-overlay" @click.self="showSaveModal = false" @keydown.escape.window="showSaveModal = false">
<div class="modal">
<div class="modal-header"><h3>Save Workflow</h3><button class="modal-close" @click="showSaveModal = false">&times;</button></div>
<div class="form-group"><label>Name</label><input class="form-input" x-model="workflowName" placeholder="my-workflow"></div>
<div class="form-group"><label>Description</label><input class="form-input" x-model="workflowDescription" placeholder="What does this workflow do?"></div>
<div class="text-xs text-dim mb-4" x-text="nodes.filter(function(n){return n.type!=='start'&&n.type!=='end'}).length + ' steps, ' + connections.length + ' connections'"></div>
<button class="btn btn-primary btn-block" @click="saveWorkflow()">Save</button>
</div>
</div>
</template>
</div>
</template>
</div>
</template>
<!-- Page: Scheduler -->
<template x-if="page === 'scheduler'">
<div x-data="schedulerPage()">
<div class="page-header">
<h2>Scheduler</h2>
<div class="flex gap-2">
<button class="btn btn-primary btn-sm" x-show="tab === 'jobs'" @click="showCreateForm = true">+ New Job</button>
</div>
</div>
<div class="tabs" role="tablist">
<div class="tab" role="tab" :class="{ active: tab === 'jobs' }" @click="tab = 'jobs'">
Scheduled Jobs <span class="badge badge-dim" x-show="jobs.length" x-text="jobCount() + '/' + jobs.length + ' active'" style="margin-left:4px"></span>
</div>
<div class="tab" role="tab" :class="{ active: tab === 'triggers' }" @click="tab = 'triggers'; if (!triggers.length && !trigLoading) loadTriggers()">
Event Triggers <span class="badge badge-dim" x-show="triggers.length" x-text="triggers.length" style="margin-left:4px"></span>
</div>
<div class="tab" :class="{ active: tab === 'history' }" @click="tab = 'history'; loadHistory()">
Run History
</div>
</div>
<div class="page-body" x-init="loadData()">
<!-- ── TAB: Scheduled Jobs ── -->
<div x-show="tab === 'jobs'">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading scheduled jobs...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<!-- Explainer -->
<div class="card mb-4" style="border-left:3px solid var(--accent)">
<div class="font-bold" style="font-size:13px;margin-bottom:4px">Scheduled Jobs</div>
<div class="text-sm text-dim" style="line-height:1.6">
Create cron-based scheduled jobs that send messages to agents on a recurring schedule.
Use cron expressions like <code style="color:var(--accent)">*/5 * * * *</code> (every 5 min) or
<code style="color:var(--accent)">0 9 * * 1-5</code> (weekdays at 9am). You can also run any job
manually with the "Run Now" button.
</div>
</div>
<!-- Jobs Table -->
<div class="table-wrap" x-show="jobs.length">
<table>
<thead>
<tr>
<th>Name</th>
<th>Schedule</th>
<th>Agent</th>
<th>Status</th>
<th>Last Run</th>
<th>Next Run</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<template x-for="job in jobs" :key="job.id">
<tr>
<td>
<span class="font-bold" x-text="job.name || job.description || '(unnamed)'"></span>
<div class="text-xs text-dim" x-show="job.message" x-text="(job.message || '').substring(0, 60) + ((job.message || '').length > 60 ? '...' : '')" :title="job.message"></div>
</td>
<td>
<code style="font-size:11px;color:var(--accent)" x-text="job.cron"></code>
<div class="text-xs text-dim" x-text="describeCron(job.cron)"></div>
</td>
<td class="truncate" style="max-width:120px" x-text="agentName(job.agent_id || job.agent)" :title="job.agent_id || job.agent"></td>
<td>
<span class="badge" :class="job.enabled ? 'badge-success' : 'badge-dim'" x-text="job.enabled ? 'Active' : 'Paused'"></span>
</td>
<td class="text-xs" :title="formatTime(job.last_run)" x-text="relativeTime(job.last_run)"></td>
<td class="text-xs" :title="formatTime(job.next_run)" x-text="relativeTime(job.next_run)"></td>
<td>
<div class="flex gap-1">
<button class="btn btn-primary btn-sm" @click="runNow(job)" :disabled="runningJobId === job.id">
<span x-show="runningJobId !== job.id">Run</span>
<span x-show="runningJobId === job.id">...</span>
</button>
<button class="btn btn-ghost btn-sm" @click="toggleJob(job)" x-text="job.enabled ? 'Pause' : 'Enable'"></button>
<button class="btn btn-danger btn-sm" @click="deleteJob(job)">Del</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Empty State -->
<div class="empty-state" x-show="!jobs.length">
<h4>No scheduled jobs</h4>
<p class="hint">Create a cron job to run agents on a recurring schedule. Jobs are stored persistently and survive restarts.</p>
<button class="btn btn-primary mt-4" @click="showCreateForm = true">+ Create Scheduled Job</button>
</div>
</div>
<!-- Create Job Modal -->
<template x-if="showCreateForm">
<div class="modal-overlay" @click.self="showCreateForm = false" @keydown.escape.window="showCreateForm = false">
<div class="modal">
<div class="modal-header">
<h3>Create Scheduled Job</h3>
<button class="modal-close" @click="showCreateForm = false">&times;</button>
</div>
<div class="form-group">
<label>Job Name</label>
<input class="form-input" x-model="newJob.name" placeholder="daily-report">
</div>
<div class="form-group">
<label>Cron Expression</label>
<input class="form-input" x-model="newJob.cron" placeholder="0 9 * * 1-5" style="font-family:monospace">
<div class="text-xs text-dim mt-1" x-show="newJob.cron" x-text="describeCron(newJob.cron)"></div>
<div class="text-xs text-dim mt-1">Format: <code>minute hour day-of-month month day-of-week</code></div>
</div>
<div class="form-group">
<label>Quick Presets</label>
<div class="flex gap-1 flex-wrap">
<template x-for="preset in cronPresets" :key="preset.cron">
<button class="btn btn-sm" :class="newJob.cron === preset.cron ? 'btn-primary' : 'btn-ghost'" @click="applyCronPreset(preset)" x-text="preset.label"></button>
</template>
</div>
</div>
<div class="form-group">
<label>Target Agent</label>
<select class="form-select" x-model="newJob.agent_id">
<option value="">Any available agent</option>
<template x-for="a in availableAgents" :key="a.id">
<option :value="a.id" x-text="a.name + ' (' + (a.model_provider || 'unknown') + ':' + (a.model_name || 'unknown') + ')'"></option>
</template>
</select>
<div class="text-xs text-dim mt-1" x-show="!availableAgents.length">No agents running. <a href="#agents" style="color:var(--accent)">Spawn one first.</a></div>
</div>
<div class="form-group">
<label>Message to Send</label>
<textarea class="form-textarea" x-model="newJob.message" placeholder="Generate and email the daily status report..." rows="3"></textarea>
<div class="text-xs text-dim mt-1">The message sent to the agent each time this job runs.</div>
</div>
<div class="form-group">
<label class="flex items-center gap-2">
<div class="toggle" :class="{ active: newJob.enabled }" @click="newJob.enabled = !newJob.enabled"></div>
<span x-text="newJob.enabled ? 'Enabled (will start running immediately)' : 'Disabled (create paused)'"></span>
</label>
</div>
<button class="btn btn-primary btn-block mt-4" @click="createJob()" :disabled="creating">
<span x-show="!creating">Create Schedule</span>
<span x-show="creating">Creating...</span>
</button>
</div>
</div>
</template>
</div>
<!-- ── TAB: Event Triggers ── -->
<div x-show="tab === 'triggers'">
<div x-show="trigLoading" class="loading-state"><div class="spinner"></div><span>Loading triggers...</span></div>
<div x-show="!trigLoading && trigLoadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="trigLoadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadTriggers()">Retry</button>
</div>
<div x-show="!trigLoading && !trigLoadError">
<div class="card mb-4" style="border-left:3px solid var(--accent)">
<div class="font-bold" style="font-size:13px;margin-bottom:4px">Event Triggers</div>
<div class="text-sm text-dim" style="line-height:1.6">
Event triggers fire agents in response to system events (agent lifecycle, memory updates, custom events).
Create and manage triggers on the <a href="#workflows" style="color:var(--accent)">Workflows</a> page.
This view shows all active triggers for monitoring.
</div>
</div>
<div class="table-wrap" x-show="triggers.length">
<table>
<thead><tr><th>Agent</th><th>Pattern</th><th>Prompt</th><th>Fires</th><th>Enabled</th><th>Created</th><th>Actions</th></tr></thead>
<tbody>
<template x-for="t in triggers" :key="t.id">
<tr>
<td class="font-bold truncate" style="max-width:120px" x-text="agentName(t.agent_id)" :title="t.agent_id"></td>
<td><span class="badge badge-created trigger-type" x-text="triggerType(t.pattern)"></span></td>
<td class="truncate text-xs text-dim" style="max-width:180px" x-text="t.prompt_template" :title="t.prompt_template"></td>
<td x-text="t.fire_count + (t.max_fires > 0 ? '/' + t.max_fires : '')"></td>
<td>
<div class="toggle" :class="{ active: t.enabled }" @click="toggleTrigger(t)"></div>
</td>
<td class="text-xs" x-text="new Date(t.created_at).toLocaleDateString()"></td>
<td>
<button class="btn btn-danger btn-sm" @click="deleteTrigger(t)">Delete</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!triggers.length">
<h4>No event triggers</h4>
<p class="hint">Create event triggers on the <a href="#workflows" style="color:var(--accent)">Workflows page</a> to fire agents in response to system events.</p>
</div>
</div>
</div>
<!-- ── TAB: Run History ── -->
<div x-show="tab === 'history'">
<div x-show="historyLoading" class="loading-state"><div class="spinner"></div><span>Loading run history...</span></div>
<div x-show="!historyLoading">
<div class="card mb-4" style="border-left:3px solid var(--accent)">
<div class="font-bold" style="font-size:13px;margin-bottom:4px">Run History</div>
<div class="text-sm text-dim" style="line-height:1.6">
Recent executions of scheduled jobs and event trigger fires. History is aggregated from schedule run counts and trigger fire counts.
</div>
</div>
<div class="table-wrap" x-show="history.length">
<table>
<thead><tr><th>Time</th><th>Name</th><th>Type</th><th>Status</th><th>Total Runs</th></tr></thead>
<tbody>
<template x-for="(h, idx) in history" :key="idx">
<tr>
<td class="text-xs" style="white-space:nowrap" x-text="formatTime(h.timestamp)"></td>
<td class="font-bold" x-text="h.name"></td>
<td><span class="badge" :class="h.type === 'schedule' ? 'badge-created' : 'badge-dim'" x-text="h.type === 'schedule' ? 'Cron Job' : 'Trigger'"></span></td>
<td><span class="badge badge-success" x-text="h.status"></span></td>
<td x-text="h.run_count"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!history.length">
<h4>No run history yet</h4>
<p class="hint">Run history will appear here after scheduled jobs or triggers execute.</p>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Page: Channels -->
<template x-if="page === 'channels'">
<div x-data="channelsPage">
<div class="page-header">
<h2>Channels <span class="badge badge-muted" x-text="configuredCount + '/' + allChannels.length + ' configured'"></span></h2>
</div>
<div class="page-body" x-init="loadChannels()">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading channels...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<!-- Category tabs -->
<div class="flex gap-2 mb-4" style="flex-wrap:wrap">
<template x-for="cat in categories" :key="cat.key">
<button class="btn btn-sm"
:class="categoryFilter === cat.key ? 'btn-primary' : 'btn-ghost'"
@click="categoryFilter = cat.key"
x-text="cat.label + ' (' + categoryCount(cat.key) + ')'">
</button>
</template>
</div>
<!-- Search bar -->
<div class="mb-4">
<input class="form-input" type="text" placeholder="Search channels..."
x-model="searchQuery" style="max-width:400px">
</div>
<!-- Channel card grid -->
<div class="card-grid">
<template x-for="ch in filteredChannels" :key="ch.name">
<div class="card" :class="{ 'card-unconfigured': !ch.configured }" style="cursor:pointer" @click="openSetup(ch)">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2">
<span class="channel-icon" x-text="ch.icon"></span>
<div class="card-header" style="margin:0" x-text="ch.display_name"></div>
</div>
<span class="badge" :class="statusBadge(ch).cls" x-text="statusBadge(ch).text"></span>
</div>
<div class="card-meta" x-text="ch.description"></div>
<div class="flex justify-between items-center mt-2">
<span class="difficulty-badge" :class="difficultyClass(ch.difficulty)" x-text="ch.difficulty + ' · ' + ch.setup_time"></span>
<button class="btn btn-ghost btn-sm" @click.stop="openSetup(ch)" x-text="ch.configured ? 'Edit' : 'Set up'"></button>
</div>
</div>
</template>
</div>
<div x-show="filteredChannels.length === 0" class="text-dim mt-4" style="text-align:center">
<p>No channels match your search.</p>
</div>
</div>
<!-- Setup modal (OpenClaw-style with QR support + 3-step flow) -->
<template x-if="setupModal">
<div class="modal-overlay" @click.self="setupModal = null" @keydown.escape.window="setupModal = null">
<div class="modal" style="max-width:480px">
<div class="modal-header">
<div>
<h3 style="display:flex;align-items:center;gap:0.5rem">
<span class="channel-icon" x-text="setupModal.icon" style="font-size:1rem"></span>
<span x-text="setupModal.display_name"></span>
</h3>
<div class="text-xs text-dim mt-1" x-text="setupModal.quick_setup || setupModal.description"></div>
</div>
<button class="modal-close" @click="setupModal = null">&times;</button>
</div>
<!-- 3-step progress indicator (non-QR channels only) -->
<div class="channel-steps" x-show="!isQrChannel()">
<div class="channel-step-item">
<div class="channel-step-num" :class="{ active: setupStep === 1, done: setupStep > 1 }">
<span x-show="setupStep <= 1">1</span><span x-show="setupStep > 1">&#10003;</span>
</div>
<span class="channel-step-label" :class="{ active: setupStep === 1, done: setupStep > 1 }">Configure</span>
</div>
<div class="channel-step-line" :class="{ done: setupStep > 1 }"></div>
<div class="channel-step-item">
<div class="channel-step-num" :class="{ active: setupStep === 2, done: setupStep > 2 }">
<span x-show="setupStep <= 2">2</span><span x-show="setupStep > 2">&#10003;</span>
</div>
<span class="channel-step-label" :class="{ active: setupStep === 2, done: setupStep > 2 }">Verify</span>
</div>
<div class="channel-step-line" :class="{ done: setupStep > 2 }"></div>
<div class="channel-step-item">
<div class="channel-step-num" :class="{ active: setupStep === 3, done: setupStep >= 3 }">
<span x-show="setupStep < 3">3</span><span x-show="setupStep >= 3">&#10003;</span>
</div>
<span class="channel-step-label" :class="{ active: setupStep === 3, done: setupStep >= 3 }">Ready</span>
</div>
</div>
<!-- Ready panel (step 3) for non-QR channels -->
<div x-show="!isQrChannel() && setupStep === 3 && testPassed" class="ready-panel">
<div class="ready-panel-icon">&#10003;</div>
<div class="ready-panel-title" x-text="setupModal.display_name + ' is ready!'"></div>
<div class="ready-panel-desc">
Your channel is configured and verified. It will activate automatically.
</div>
<div class="flex gap-2 mt-4" style="justify-content:center">
<button class="btn btn-ghost btn-sm" @click="setupStep = 1">Edit Config</button>
<button class="btn btn-primary btn-sm" @click="setupModal = null">Done</button>
</div>
</div>
<!-- ═══ QR CODE FLOW (WhatsApp Web style) ═══ -->
<template x-if="isQrChannel() && !showBusinessApi">
<div>
<!-- QR: Loading -->
<div x-show="qr.loading" style="text-align:center;padding:2rem 0">
<div class="spinner"></div>
<p class="text-sm text-dim mt-2">Connecting to WhatsApp Web gateway...</p>
</div>
<!-- QR: Gateway available — show QR code -->
<div x-show="!qr.loading && qr.available && qr.dataUrl && !qr.connected" style="text-align:center">
<div style="background:#fff;display:inline-block;padding:1rem;border-radius:12px;margin:0.5rem 0">
<img :src="qr.dataUrl" alt="WhatsApp QR Code" style="width:256px;height:256px;image-rendering:pixelated">
</div>
<ol style="text-align:left;font-size:0.85rem;margin:1rem 0;padding-left:1.5rem;opacity:0.8">
<template x-for="(step, i) in setupModal.setup_steps || []" :key="i">
<li x-text="step" style="margin-bottom:0.25rem"></li>
</template>
</ol>
<p class="text-xs text-dim" x-text="qr.message"></p>
<button class="btn btn-ghost btn-sm mt-2" x-show="qr.expired" @click="startQR()">Generate New QR</button>
</div>
<!-- QR: Connected! -->
<div x-show="!qr.loading && qr.connected" style="text-align:center;padding:2rem 0">
<div style="font-size:3rem;margin-bottom:0.5rem">&#10003;</div>
<p class="text-sm" style="font-weight:600" x-text="qr.message || 'WhatsApp linked successfully!'"></p>
<p class="text-xs text-dim mt-1">Channel will activate automatically.</p>
</div>
<!-- QR: Gateway not available — show setup hint -->
<div x-show="!qr.loading && !qr.available" style="padding:1rem 0">
<div style="background:var(--bg-secondary,#1a1a2e);border-radius:8px;padding:1.25rem;text-align:center">
<div style="font-size:2rem;margin-bottom:0.5rem;opacity:0.5">&#128241;</div>
<p class="text-sm" x-text="qr.message || 'WhatsApp Web gateway not available'"></p>
<p class="text-xs text-dim mt-2" x-show="qr.help" x-text="qr.help"></p>
<p class="text-xs text-dim mt-1" x-show="qr.error" style="color:var(--red,#ef4444)" x-text="qr.error"></p>
</div>
<p class="text-xs text-dim mt-3" style="text-align:center">
Or use the <button class="btn-link" @click="showBusinessApi = true" style="font-size:inherit;text-decoration:underline;cursor:pointer;background:none;border:none;color:var(--accent,#818cf8);padding:0">Business API</button> with a Meta developer account.
</p>
</div>
<!-- QR: Switch to Business API link (always visible when gateway is available) -->
<div x-show="!qr.loading && qr.available" class="text-xs text-dim mt-2" style="text-align:center">
Have a Meta Business account? <button class="btn-link" @click="showBusinessApi = true" style="font-size:inherit;text-decoration:underline;cursor:pointer;background:none;border:none;color:var(--accent,#818cf8);padding:0">Use Business API instead</button>
</div>
<!-- Action buttons for QR mode -->
<div class="flex gap-2 mt-4" style="flex-wrap:wrap;justify-content:center" x-show="!qr.loading">
<button class="btn btn-ghost" x-show="qr.available && !qr.connected && !qr.expired" @click="startQR()">Refresh QR</button>
<button class="btn btn-ghost" x-show="setupModal.configured" @click="testChannel()"
:disabled="testing[setupModal.name]"
x-text="testing[setupModal.name] ? 'Testing...' : 'Test Connection'">
</button>
<button class="btn btn-ghost" style="color:var(--red,#ef4444)" x-show="setupModal.configured" @click="removeChannel()">Remove</button>
</div>
</div>
</template>
<!-- ═══ STANDARD FORM FLOW (for non-QR channels, or Business API fallback) ═══ -->
<template x-if="(!isQrChannel() || showBusinessApi) && !(setupStep === 3 && testPassed && !isQrChannel())">
<div>
<!-- Back to QR link (only for QR channels in Business API mode) -->
<div x-show="isQrChannel() && showBusinessApi" class="mb-3">
<button class="btn btn-ghost btn-sm" @click="showBusinessApi = false" style="font-size:0.8rem">&larr; Back to QR scan</button>
<p class="text-xs text-dim mt-1">Configure via WhatsApp Cloud API (requires a Meta Business developer account).</p>
</div>
<!-- Quick setup steps (collapsed, minimal) -->
<details class="mb-4" style="font-size:0.8rem" x-show="!isQrChannel()">
<summary class="text-dim" style="cursor:pointer">How to get credentials</summary>
<ol class="setup-steps" style="margin-top:0.5rem">
<template x-for="(step, i) in setupModal.setup_steps || []" :key="i">
<li class="text-sm" x-text="step"></li>
</template>
</ol>
</details>
<!-- Basic fields only (the minimum to get started) -->
<template x-for="f in (isQrChannel() && showBusinessApi) ? advancedFields() : basicFields()" :key="f.key">
<div style="margin-bottom:0.75rem">
<label class="text-sm" style="display:block;margin-bottom:0.25rem" x-text="f.label + (f.required ? ' *' : '')"></label>
<template x-if="f.type === 'secret'">
<input class="form-input" type="password"
x-model="formValues[f.key]"
:placeholder="f.has_value ? '••••••• (set — leave blank to keep)' : f.placeholder"
:required="f.required">
</template>
<template x-if="f.type === 'number'">
<input class="form-input" type="number"
x-model="formValues[f.key]"
:placeholder="f.placeholder">
</template>
<template x-if="f.type === 'text' || f.type === 'list'">
<input class="form-input" type="text"
x-model="formValues[f.key]"
:placeholder="f.type === 'list' ? f.placeholder + ' (comma-separated)' : f.placeholder">
</template>
<div class="text-xs text-dim" style="margin-top:2px"
x-show="f.env_var && f.has_value"
x-text="f.env_var + ' is set'"></div>
</div>
</template>
<!-- Advanced toggle (only for non-QR channels) -->
<template x-if="!isQrChannel() && hasAdvanced()">
<div>
<button class="btn btn-ghost btn-sm mb-2" @click="showAdvanced = !showAdvanced"
x-text="showAdvanced ? 'Hide advanced' : 'Show advanced (' + advancedFields().length + ')'"></button>
<template x-if="showAdvanced">
<div>
<template x-for="f in advancedFields()" :key="f.key">
<div style="margin-bottom:0.75rem">
<label class="text-sm text-dim" style="display:block;margin-bottom:0.25rem" x-text="f.label"></label>
<template x-if="f.type === 'secret'">
<input class="form-input" type="password"
x-model="formValues[f.key]"
:placeholder="f.has_value ? '••••••• (set)' : f.placeholder">
</template>
<template x-if="f.type === 'number'">
<input class="form-input" type="number"
x-model="formValues[f.key]"
:placeholder="f.placeholder">
</template>
<template x-if="f.type === 'text' || f.type === 'list'">
<input class="form-input" type="text"
x-model="formValues[f.key]"
:placeholder="f.type === 'list' ? f.placeholder + ' (comma-separated)' : f.placeholder">
</template>
</div>
</template>
</div>
</template>
</div>
</template>
<!-- Action buttons -->
<div class="flex gap-2 mt-4" style="flex-wrap:wrap">
<button class="btn btn-primary" @click="saveChannel()" :disabled="configuring"
x-text="configuring ? 'Saving...' : (setupModal.configured ? 'Update' : 'Save & Test')">
</button>
<button class="btn btn-ghost" x-show="setupModal.configured" @click="testChannel()"
:disabled="testing[setupModal.name]"
x-text="testing[setupModal.name] ? 'Testing...' : 'Test'">
</button>
<button class="btn btn-ghost" style="color:var(--red,#ef4444)" x-show="setupModal.configured" @click="removeChannel()">Remove</button>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Page: Skills -->
<template x-if="page === 'skills'">
<div x-data="skillsPage">
<div class="page-header"><h2>Skills</h2></div>
<div class="page-body" x-init="loadSkills()">
<div class="info-card">
<h4>Skills &amp; Ecosystem</h4>
<p>Skills extend your agents with new capabilities. OpenFang supports the <strong>OpenClaw/ClawHub</strong> ecosystem (3,000+ community skills) plus local skills.</p>
<ul>
<li><strong>Prompt-only</strong> &mdash; inject context and instructions into the agent's system prompt (most ClawHub skills)</li>
<li><strong>Python / Node.js</strong> &mdash; executable tools that agents can call during conversations</li>
<li><strong>MCP Servers</strong> &mdash; external tools via Model Context Protocol (GitHub, filesystem, databases, etc.)</li>
</ul>
</div>
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading skills...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<!-- Main tabs -->
<div class="tabs" role="tablist">
<div class="tab" role="tab" :class="{ active: tab === 'installed' }" @click="tab = 'installed'">
Installed <span class="badge badge-dim" x-show="skills.length" x-text="skills.length" style="margin-left:4px"></span>
</div>
<div class="tab" :class="{ active: tab === 'clawhub' }" @click="tab = 'clawhub'; if (!clawhubBrowseResults.length && !clawhubSearch) browseClawHub('trending')">ClawHub</div>
<div class="tab" :class="{ active: tab === 'mcp' }" @click="tab = 'mcp'; loadMcpServers()">MCP Servers</div>
<div class="tab" :class="{ active: tab === 'create' }" @click="tab = 'create'">Quick Start</div>
</div>
<!-- TAB: Installed Skills -->
<div x-show="tab === 'installed'">
<div class="card-grid" x-show="skills.length">
<template x-for="skill in skills" :key="skill.name">
<div class="card">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2">
<div class="card-header" style="margin:0" x-text="skill.name"></div>
<span class="runtime-badge" :class="runtimeBadge(skill.runtime).cls" x-text="runtimeBadge(skill.runtime).text"></span>
<span class="badge" :class="sourceBadge(skill.source).cls" x-text="sourceBadge(skill.source).text" style="font-size:0.65rem"></span>
</div>
<div class="toggle" :class="{ active: skill.enabled }" @click="skill.enabled = !skill.enabled"></div>
</div>
<div class="card-meta" x-text="skill.description"></div>
<div class="flex justify-between items-center mt-2">
<div class="flex gap-2 items-center">
<span class="text-xs text-dim" x-text="skill.tools_count + ' tool(s)'"></span>
<span class="text-xs text-dim" x-show="skill.version" x-text="'v' + skill.version"></span>
<span class="text-xs text-dim" x-show="skill.has_prompt_context">(prompt context)</span>
</div>
<button class="btn btn-danger btn-sm" @click="uninstallSkill(skill.name)">Uninstall</button>
</div>
</div>
</template>
</div>
<div class="empty-state" x-show="!skills.length">
<div class="empty-state-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/></svg>
</div>
<h3>No skills installed</h3>
<p>Skills add new capabilities to your agents. Browse ClawHub for 3,000+ community skills or create your own.</p>
<div class="flex gap-2">
<button class="btn btn-primary" @click="tab = 'clawhub'; if (!clawhubBrowseResults.length) browseClawHub('trending')">Browse ClawHub</button>
<button class="btn btn-ghost" @click="tab = 'create'">Quick Start</button>
</div>
</div>
</div>
<!-- TAB: ClawHub (OpenClaw Ecosystem) -->
<div x-show="tab === 'clawhub'">
<!-- Search bar with live search and clear button -->
<div class="search-input mb-4" style="position:relative">
<span style="color:var(--text-muted)"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></span>
<input placeholder="Search ClawHub skills... (type to search)" x-model="clawhubSearch" @input="onSearchInput()" @keydown.enter="searchClawHub()" @keydown.escape="clearSearch()" x-ref="clawhubSearchInput">
<button x-show="clawhubSearch" @click="clearSearch()" class="search-clear-btn" title="Clear search (Esc)">&times;</button>
</div>
<!-- Sort pills (always visible when not searching) -->
<div class="filter-pills mb-4" x-show="!clawhubSearch">
<span class="filter-pill" :class="{ active: clawhubSort === 'trending' }" @click="browseClawHub('trending')">Trending</span>
<span class="filter-pill" :class="{ active: clawhubSort === 'downloads' }" @click="browseClawHub('downloads')">Most Downloaded</span>
<span class="filter-pill" :class="{ active: clawhubSort === 'stars' }" @click="browseClawHub('stars')">Most Starred</span>
<span class="filter-pill" :class="{ active: clawhubSort === 'updated' }" @click="browseClawHub('updated')">Recently Updated</span>
</div>
<!-- Category quick-search chips (always visible when not searching) -->
<div class="mb-4" x-show="!clawhubSearch">
<div class="text-xs text-dim mb-2" style="letter-spacing:0.5px">CATEGORIES</div>
<div class="flex flex-wrap gap-1">
<template x-for="cat in categories" :key="cat.id">
<span class="filter-pill" style="font-size:0.7rem;padding:2px 8px" @click="searchCategory(cat)" x-text="cat.name"></span>
</template>
</div>
</div>
<!-- Loading spinner -->
<div x-show="clawhubLoading" class="loading-state"><div class="spinner"></div><span x-text="clawhubSearch ? 'Searching ClawHub...' : 'Loading skills...'"></span></div>
<!-- Error -->
<div x-show="clawhubError && !clawhubLoading" class="error-state">
<span class="error-icon">!</span>
<p x-text="clawhubError"></p>
<p class="hint mt-2">ClawHub may be temporarily unavailable. The OpenClaw ecosystem is hosted at clawhub.ai.</p>
<button class="btn btn-ghost btn-sm mt-2" @click="clawhubSearch ? searchClawHub() : browseClawHub(clawhubSort)">Retry</button>
</div>
<!-- Search results -->
<div x-show="clawhubSearch && clawhubResults.length && !clawhubLoading">
<div class="flex justify-between items-center mb-3">
<div class="text-sm text-dim" x-text="clawhubResults.length + ' result(s) for &quot;' + clawhubSearch + '&quot;'"></div>
<button class="btn btn-ghost btn-sm" @click="clearSearch()">Clear search</button>
</div>
<div class="card-grid">
<template x-for="skill in clawhubResults" :key="skill.slug">
<div class="card card-glow" @click="showSkillDetail(skill.slug)" style="cursor:pointer">
<div class="flex justify-between items-center mb-2">
<div class="card-header" style="margin:0" x-text="skill.name || skill.slug"></div>
<span class="badge badge-info" style="font-size:0.6rem">ClawHub</span>
</div>
<div class="card-meta" x-text="skill.description" style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden"></div>
<div class="flex justify-between items-center mt-3">
<div class="flex gap-3 items-center">
<span class="text-xs text-dim" x-show="skill.version" x-text="'v' + skill.version"></span>
</div>
<button class="btn btn-primary btn-sm" @click.stop="installFromClawHub(skill.slug)" :disabled="installingSlug === skill.slug || isSkillInstalled(skill.slug)" x-text="isSkillInstalled(skill.slug) ? 'Installed' : installingSlug === skill.slug ? 'Installing...' : 'Install'"></button>
</div>
</div>
</template>
</div>
</div>
<!-- Browse results (when no search query) -->
<div x-show="!clawhubSearch && clawhubBrowseResults.length && !clawhubLoading">
<div class="card-grid">
<template x-for="skill in clawhubBrowseResults" :key="skill.slug">
<div class="card card-glow" @click="showSkillDetail(skill.slug)" style="cursor:pointer">
<div class="flex justify-between items-center mb-2">
<div class="card-header" style="margin:0" x-text="skill.name || skill.slug"></div>
<span class="badge badge-info" style="font-size:0.6rem">ClawHub</span>
</div>
<div class="card-meta" x-text="skill.description" style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden"></div>
<div class="flex justify-between items-center mt-3">
<div class="flex gap-3 items-center">
<span class="text-xs text-dim" x-show="skill.downloads" x-text="formatDownloads(skill.downloads) + ' downloads'"></span>
<span class="text-xs text-dim" x-show="skill.stars" x-text="skill.stars + ' stars'"></span>
<span class="text-xs text-dim" x-show="skill.version" x-text="'v' + skill.version"></span>
</div>
<button class="btn btn-primary btn-sm" @click.stop="installFromClawHub(skill.slug)" :disabled="installingSlug === skill.slug || isSkillInstalled(skill.slug)" x-text="isSkillInstalled(skill.slug) ? 'Installed' : installingSlug === skill.slug ? 'Installing...' : 'Install'"></button>
</div>
</div>
</template>
</div>
<!-- Load more button -->
<div class="text-center mt-4" x-show="clawhubNextCursor">
<button class="btn btn-ghost" @click="loadMoreClawHub()" :disabled="clawhubLoading">Load More</button>
</div>
</div>
<!-- Empty search results -->
<div class="empty-state" x-show="clawhubSearch && !clawhubResults.length && !clawhubLoading && !clawhubError">
<p>No skills found for "<span x-text="clawhubSearch"></span>"</p>
<p class="hint mt-1">Try a different search term or browse by category.</p>
<button class="btn btn-ghost btn-sm mt-2" @click="clearSearch()">Back to browse</button>
</div>
</div>
<!-- TAB: MCP Servers -->
<div x-show="tab === 'mcp'">
<div class="info-card">
<h4>MCP Servers (Model Context Protocol)</h4>
<p>MCP servers provide external tools to your agents &mdash; GitHub, filesystem, databases, APIs, and more. OpenFang is compatible with all OpenClaw MCP servers.</p>
<p class="mt-2" style="font-size:0.8rem;color:var(--text-dim)">Configure MCP servers in your <code>config.toml</code> under <code>[mcp_servers]</code>.</p>
</div>
<div x-show="mcpLoading" class="loading-state"><div class="spinner"></div><span>Loading MCP servers...</span></div>
<div x-show="!mcpLoading">
<!-- Connected servers -->
<div x-show="mcpServers.total_connected > 0" class="mb-4">
<div class="text-sm font-bold mb-2" style="color:var(--text-dim);letter-spacing:0.5px">CONNECTED SERVERS</div>
<div class="card-grid">
<template x-for="srv in mcpServers.connected" :key="srv.name">
<div class="card">
<div class="flex justify-between items-center mb-2">
<div class="card-header" style="margin:0" x-text="srv.name"></div>
<span class="badge badge-success">Connected</span>
</div>
<div class="card-meta" x-text="srv.tools_count + ' tool(s) available'"></div>
<div class="mt-2" x-show="srv.tools && srv.tools.length">
<div class="text-xs text-dim mb-1">Tools:</div>
<template x-for="tool in srv.tools.slice(0, 10)" :key="tool.name">
<div class="text-xs" style="padding:2px 0">
<code x-text="tool.name" style="font-size:0.7rem"></code>
<span class="text-dim" x-show="tool.description" x-text="' — ' + tool.description" style="font-size:0.65rem"></span>
</div>
</template>
<div class="text-xs text-dim" x-show="srv.tools.length > 10" x-text="'... and ' + (srv.tools.length - 10) + ' more'"></div>
</div>
</div>
</template>
</div>
</div>
<!-- Configured but not connected -->
<div x-show="mcpServers.total_configured > 0" class="mb-4">
<div class="text-sm font-bold mb-2" style="color:var(--text-dim);letter-spacing:0.5px">CONFIGURED SERVERS</div>
<div class="card-grid">
<template x-for="srv in mcpServers.configured" :key="srv.name">
<div class="card card-unconfigured">
<div class="flex justify-between items-center mb-2">
<div class="card-header" style="margin:0" x-text="srv.name"></div>
<span class="badge badge-dim" x-text="srv.transport.type"></span>
</div>
<div class="text-xs" x-show="srv.transport.type === 'stdio'">
<code x-text="srv.transport.command + ' ' + (srv.transport.args || []).join(' ')" style="font-size:0.7rem"></code>
</div>
<div class="text-xs" x-show="srv.transport.type === 'sse'">
<code x-text="srv.transport.url" style="font-size:0.7rem"></code>
</div>
<div class="text-xs text-dim mt-1" x-show="srv.env && srv.env.length" x-text="'Env: ' + srv.env.join(', ')"></div>
</div>
</template>
</div>
</div>
<!-- Empty state -->
<div class="empty-state" x-show="mcpServers.total_configured === 0 && mcpServers.total_connected === 0">
<h4>No MCP servers configured</h4>
<p class="hint">MCP servers extend your agents with external tools. Add servers to your config.toml:</p>
<pre style="text-align:left;font-size:0.75rem;margin-top:8px;background:var(--bg-secondary);padding:12px;border-radius:6px;max-width:400px;margin-left:auto;margin-right:auto">[[mcp_servers]]
name = "filesystem"
timeout_secs = 30
[mcp_servers.transport]
type = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
<p class="hint mt-2" style="font-size:0.75rem">OpenFang supports all OpenClaw-compatible MCP servers.</p>
</div>
</div>
</div>
<!-- TAB: Quick Start (Create prompt-only skills) -->
<div x-show="tab === 'create'">
<div class="info-card">
<h4>Quick Start Skills</h4>
<p>Create prompt-only skills with one click. These inject context into your agent's system prompt &mdash; no code required. Perfect for adding domain expertise or workflow guidelines.</p>
</div>
<div class="card-grid">
<template x-for="qs in quickStartSkills" :key="qs.name">
<div class="card card-glow">
<div class="flex justify-between items-center mb-2">
<div class="card-header" style="margin:0" x-text="qs.name"></div>
<span class="runtime-badge runtime-badge-prompt">PROMPT</span>
</div>
<div class="card-meta" x-text="qs.description"></div>
<div class="flex justify-end mt-2">
<button class="btn btn-ghost btn-sm" @click="createDemoSkill(qs)" :disabled="isSkillInstalledByName(qs.name)" x-text="isSkillInstalledByName(qs.name) ? 'Created' : 'Create Skill'"></button>
</div>
</div>
</template>
</div>
</div>
<!-- Skill Detail Modal -->
<template x-if="skillDetail || detailLoading">
<div class="modal-overlay" @click.self="closeDetail()" @keydown.escape.window="closeDetail()">
<div class="modal" style="max-width:600px">
<!-- Loading state -->
<div x-show="detailLoading" class="loading-state" style="padding:40px 0"><div class="spinner"></div><span>Loading skill details...</span></div>
<!-- Loaded content -->
<template x-if="skillDetail">
<div>
<div class="modal-header">
<h3 x-text="skillDetail.name || skillDetail.slug"></h3>
<button class="modal-close" @click="closeDetail()">&times;</button>
</div>
<div class="mb-3">
<div class="flex gap-2 items-center flex-wrap">
<span class="badge badge-info">ClawHub</span>
<span class="text-xs text-dim" x-show="skillDetail.version" x-text="'v' + skillDetail.version"></span>
<span class="text-xs" x-show="skillDetail.author_name || skillDetail.author" style="color:var(--text-dim)">
<span x-show="skillDetail.author_image" style="display:inline-block;vertical-align:middle;margin-right:4px"><img :src="skillDetail.author_image" style="width:16px;height:16px;border-radius:50%"></span>
<span x-text="'by ' + (skillDetail.author_name || skillDetail.author)"></span>
</span>
</div>
</div>
<div class="flex gap-4 items-center mb-3" x-show="skillDetail.downloads || skillDetail.stars">
<span class="text-sm" x-show="skillDetail.downloads" x-text="formatDownloads(skillDetail.downloads) + ' downloads'" style="color:var(--text-dim)"></span>
<span class="text-sm" x-show="skillDetail.stars" x-text="skillDetail.stars + ' stars'" style="color:var(--text-dim)"></span>
</div>
<div class="mb-4" x-show="skillDetail.description">
<p x-text="skillDetail.description"></p>
</div>
<div class="mb-4" x-show="skillDetail.tags && typeof skillDetail.tags === 'object'">
<div class="flex flex-wrap gap-1">
<template x-for="key in Object.keys(skillDetail.tags || {})" :key="key">
<span class="category-badge" x-text="key + ': ' + skillDetail.tags[key]"></span>
</template>
</div>
</div>
<div class="mb-4" x-show="installResult && installResult.warnings && installResult.warnings.length">
<div class="form-group"><label>Security Warnings</label></div>
<template x-for="w in (installResult ? installResult.warnings : [])" :key="w.message">
<div class="text-xs" :class="w.severity === 'Critical' ? 'text-danger' : 'text-dim'" x-text="'[' + w.severity + '] ' + w.message" style="padding:2px 0"></div>
</template>
</div>
<div class="flex gap-2">
<button class="btn btn-primary btn-block" @click="installFromClawHub(skillDetail.slug)" :disabled="installingSlug === skillDetail.slug || skillDetail.installed || isSkillInstalled(skillDetail.slug)" x-text="skillDetail.installed || isSkillInstalled(skillDetail.slug) ? 'Already Installed' : installingSlug === skillDetail.slug ? 'Installing...' : 'Install from ClawHub'"></button>
</div>
<div class="text-xs text-dim mt-2 text-center">Skills are security-scanned before installation. Prompt injection and malware patterns are blocked.</div>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<!-- Page: Hands -->
<template x-if="page === 'hands'">
<div x-data="handsPage">
<div class="page-header"><h2>Hands</h2></div>
<div class="page-body" x-init="loadData()">
<div class="info-card">
<h4>Hands &mdash; Curated Autonomous Capability Packages</h4>
<p>Hands are pre-configured AI agents that autonomously handle specific tasks. Each hand includes a tuned system prompt, required tools, and a dashboard for tracking work.</p>
</div>
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading hands...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<!-- Tabs -->
<div class="tabs">
<div class="tab" :class="{ active: tab === 'available' }" @click="tab = 'available'">
Available <span class="badge badge-dim" x-show="hands.length" x-text="hands.length" style="margin-left:4px"></span>
</div>
<div class="tab" :class="{ active: tab === 'active' }" @click="tab = 'active'; loadActive()">
Active <span class="badge badge-success" x-show="instances.length" x-text="instances.length" style="margin-left:4px"></span>
</div>
</div>
<!-- TAB: Available Hands -->
<div x-show="tab === 'available'">
<div class="card-grid" x-show="hands.length">
<template x-for="hand in hands" :key="hand.id">
<div class="card" :class="{ 'card-glow': hand.requirements_met }">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2">
<span style="font-size:1.4rem" x-text="hand.icon"></span>
<div class="card-header" style="margin:0" x-text="hand.name"></div>
</div>
<span class="badge" :class="hand.requirements_met ? 'badge-success' : 'badge-dim'" x-text="hand.requirements_met ? 'Ready' : 'Setup needed'"></span>
</div>
<div class="card-meta" x-text="hand.description"></div>
<!-- Requirements -->
<div class="mt-3" x-show="hand.requirements && hand.requirements.length">
<div class="text-xs text-dim mb-1" style="letter-spacing:0.5px">REQUIREMENTS</div>
<template x-for="req in hand.requirements" :key="req.key">
<div class="flex items-center gap-2 text-xs" style="padding:2px 0">
<span :style="req.satisfied ? 'color:var(--success)' : 'color:var(--danger)'" x-text="req.satisfied ? '\u2713' : '\u2717'"></span>
<span x-text="req.label"></span>
</div>
</template>
</div>
<!-- Tools -->
<div class="mt-2">
<span class="text-xs text-dim" x-text="hand.tools.length + ' tool(s)'"></span>
<span class="text-xs text-dim" style="margin-left:8px" x-text="hand.dashboard_metrics + ' metric(s)'"></span>
<span class="category-badge" style="margin-left:8px;font-size:0.65rem" x-text="hand.category"></span>
</div>
<!-- Actions -->
<div class="flex justify-between items-center mt-3">
<button class="btn btn-ghost btn-sm" @click="showDetail(hand.id)">Details</button>
<button class="btn btn-primary btn-sm" @click="openSetupWizard(hand.id)" :disabled="setupLoading || activatingId === hand.id" x-text="setupLoading ? 'Loading...' : 'Activate'"></button>
</div>
</div>
</template>
</div>
<div class="empty-state" x-show="!hands.length">
<div class="empty-state-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2 2 2 0 0 0-2 2v6"/><path d="M10 10.5V6a2 2 0 0 0-2-2 2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.9-5.7-2.4L3.4 16a2 2 0 0 1 3.2-2.4L8 15"/></svg>
</div>
<h3>No hands available</h3>
<p>Hands are curated AI capability packages. They will appear once the kernel loads bundled hands.</p>
</div>
</div>
<!-- TAB: Active Instances -->
<div x-show="tab === 'active'">
<div x-show="activeLoading" class="loading-state"><div class="spinner"></div><span>Loading active hands...</span></div>
<div class="card-grid" x-show="instances.length && !activeLoading">
<template x-for="inst in instances" :key="inst.instance_id">
<div class="card">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2">
<span style="font-size:1.4rem" x-text="getHandIcon(inst.hand_id)"></span>
<div class="card-header" style="margin:0" x-text="inst.agent_name || inst.hand_id"></div>
</div>
<span class="badge" :class="inst.status === 'Active' ? 'badge-success' : inst.status === 'Paused' ? 'badge-dim' : 'badge-info'" x-text="inst.status"></span>
</div>
<div class="text-xs text-dim" x-text="'Activated: ' + new Date(inst.activated_at).toLocaleString()"></div>
<div class="text-xs text-dim" x-show="inst.agent_id" x-text="'Agent: ' + inst.agent_id"></div>
<!-- Stats (loaded on demand) -->
<div class="mt-3" x-show="inst._stats">
<template x-for="(val, label) in (inst._stats || {})" :key="label">
<div class="flex justify-between text-xs" style="padding:2px 0">
<span class="text-dim" x-text="label"></span>
<span x-text="formatMetric(val)"></span>
</div>
</template>
</div>
<!-- Actions -->
<div class="flex gap-2 mt-3">
<button class="btn btn-ghost btn-sm" @click="loadStats(inst)">Stats</button>
<template x-if="isBrowserHand(inst)">
<button class="btn btn-ghost btn-sm" @click="openBrowserViewer(inst)">View Browser</button>
</template>
<template x-if="inst.status === 'Active'">
<button class="btn btn-ghost btn-sm" @click="pauseHand(inst)">Pause</button>
</template>
<template x-if="inst.status === 'Paused'">
<button class="btn btn-ghost btn-sm" @click="resumeHand(inst)">Resume</button>
</template>
<button class="btn btn-danger btn-sm" @click="deactivate(inst)">Deactivate</button>
</div>
</div>
</template>
</div>
<div class="empty-state" x-show="!instances.length && !activeLoading">
<h4>No active hands</h4>
<p class="hint">Activate a hand from the Available tab to get started. Each hand spawns a dedicated agent.</p>
<button class="btn btn-primary mt-4" @click="tab = 'available'">Browse Hands</button>
</div>
</div>
<!-- Detail Modal -->
<template x-if="detailHand">
<div class="modal-overlay" @click.self="detailHand = null" @keydown.escape.window="detailHand = null">
<div class="modal" style="max-width:600px">
<div class="modal-header">
<h3><span x-text="detailHand.icon"></span> <span x-text="detailHand.name"></span></h3>
<button class="modal-close" @click="detailHand = null">&times;</button>
</div>
<div class="mb-3">
<p x-text="detailHand.description"></p>
</div>
<div class="mb-3">
<div class="text-xs text-dim mb-1" style="letter-spacing:0.5px">AGENT CONFIG</div>
<div class="text-sm" x-show="detailHand.agent">
<div class="flex justify-between" style="padding:2px 0"><span class="text-dim">Provider</span><span x-text="detailHand.agent.provider"></span></div>
<div class="flex justify-between" style="padding:2px 0"><span class="text-dim">Model</span><span x-text="detailHand.agent.model"></span></div>
</div>
</div>
<div class="mb-3">
<div class="text-xs text-dim mb-1" style="letter-spacing:0.5px">REQUIREMENTS</div>
<template x-for="req in (detailHand.requirements || [])" :key="req.key">
<div class="flex items-center gap-2 text-sm" style="padding:3px 0">
<span :style="req.satisfied ? 'color:var(--success)' : 'color:var(--danger)'" x-text="req.satisfied ? '\u2713' : '\u2717'"></span>
<span x-text="req.label"></span>
<code class="text-xs text-dim" x-text="req.check_value" style="margin-left:auto"></code>
</div>
</template>
</div>
<div class="mb-3">
<div class="text-xs text-dim mb-1" style="letter-spacing:0.5px">TOOLS</div>
<div class="flex flex-wrap gap-1">
<template x-for="tool in (detailHand.tools || [])" :key="tool">
<span class="category-badge" x-text="tool" style="font-size:0.7rem"></span>
</template>
</div>
</div>
<div class="mb-3" x-show="detailHand.dashboard && detailHand.dashboard.length">
<div class="text-xs text-dim mb-1" style="letter-spacing:0.5px">DASHBOARD METRICS</div>
<template x-for="m in (detailHand.dashboard || [])" :key="m.memory_key">
<div class="text-sm" style="padding:2px 0"><span x-text="m.label"></span> <code class="text-xs text-dim" x-text="'(' + m.format + ')'"></code></div>
</template>
</div>
<div class="flex gap-2">
<button class="btn btn-primary btn-block" @click="var hid = detailHand.id; detailHand = null; openSetupWizard(hid)" :disabled="setupLoading">Activate</button>
</div>
</div>
</div>
</template>
<!-- Setup Wizard (guided multi-step activation) -->
<template x-if="setupWizard">
<div class="modal-overlay" @click.self="closeSetupWizard()" @keydown.escape.window="closeSetupWizard()">
<div class="hand-wizard">
<!-- Header -->
<div class="hand-wizard-header">
<span class="wizard-icon" x-text="setupWizard.icon"></span>
<div>
<div class="wizard-title" x-text="'Set up ' + setupWizard.name"></div>
<div class="wizard-subtitle" x-text="setupWizard.description"></div>
</div>
<button class="wizard-close" @click="closeSetupWizard()">&times;</button>
</div>
<!-- Step Indicators -->
<div class="hand-steps">
<template x-if="setupHasReqs">
<div class="hand-step-item" :class="{ active: setupStep === 1, done: setupStep > 1 }">
<div class="hand-step-num" x-text="setupStep > 1 ? '\u2713' : '1'"></div>
<div class="hand-step-label">Dependencies</div>
</div>
</template>
<template x-if="setupHasReqs">
<div class="hand-step-line" :class="{ done: setupStep > 1 }"></div>
</template>
<div class="hand-step-item" :class="{ active: setupStep === 2, done: setupStep > 2 }">
<div class="hand-step-num" x-text="setupStep > 2 ? '\u2713' : (setupHasReqs ? '2' : '1')"></div>
<div class="hand-step-label">Configure</div>
</div>
<div class="hand-step-line" :class="{ done: setupStep > 2 }"></div>
<div class="hand-step-item" :class="{ active: setupStep === 3 }">
<div class="hand-step-num" x-text="setupHasReqs ? '3' : '2'"></div>
<div class="hand-step-label">Launch</div>
</div>
</div>
<!-- ═══ Step 1: Dependencies ═══ -->
<div class="hand-wizard-body" x-show="setupStep === 1">
<template x-for="req in (setupWizard.requirements || [])" :key="req.key">
<div class="dep-card" :class="req.satisfied ? 'dep-met' : 'dep-missing'">
<div class="dep-card-header">
<div class="dep-status-icon" :class="[req.satisfied ? 'met' : 'missing', setupChecking ? 'checking' : '']" x-text="req.satisfied ? '\u2713' : '\u2717'"></div>
<span class="dep-card-title" x-text="req.label"></span>
<template x-if="req.install && req.install.estimated_time">
<span class="dep-time-badge" x-text="req.install.estimated_time"></span>
</template>
</div>
<template x-if="req.description">
<div class="dep-card-desc" x-text="req.description"></div>
</template>
<!-- Satisfied: green message -->
<template x-if="req.satisfied">
<div class="dep-met-msg">&check; Detected on your system</div>
</template>
<!-- Not satisfied: show install instructions -->
<template x-if="!req.satisfied">
<div>
<!-- Binary/EnvVar: platform install commands -->
<template x-if="req.type !== 'ApiKey' && req.install && (req.install.macos || req.install.windows || req.install.linux_apt)">
<div class="install-block">
<div class="install-platform-pills">
<button class="install-platform-pill" :class="{ active: (installPlatforms[req.key] || detectedPlatform) === 'macos' }" @click="installPlatforms[req.key] = 'macos'" x-show="req.install.macos">macOS</button>
<button class="install-platform-pill" :class="{ active: (installPlatforms[req.key] || detectedPlatform) === 'windows' }" @click="installPlatforms[req.key] = 'windows'" x-show="req.install.windows">Windows</button>
<button class="install-platform-pill" :class="{ active: (installPlatforms[req.key] || detectedPlatform) === 'linux' }" @click="installPlatforms[req.key] = 'linux'" x-show="req.install.linux_apt || req.install.linux_dnf || req.install.linux_pacman">Linux</button>
<button class="install-platform-pill" :class="{ active: (installPlatforms[req.key] || detectedPlatform) === 'pip' }" @click="installPlatforms[req.key] = 'pip'" x-show="req.install.pip && !req.install.macos">pip</button>
</div>
<div class="install-cmd">
<code x-text="getInstallCmd(req) || 'No command available'"></code>
<button class="copy-btn" :class="{ copied: clipboardMsg === getInstallCmd(req) }" @click="copyToClipboard(getInstallCmd(req))" x-text="clipboardMsg === getInstallCmd(req) ? 'Copied!' : 'Copy'"></button>
</div>
</div>
</template>
<!-- Install steps (e.g. multi-step like playwright) -->
<template x-if="req.install && req.install.steps && req.install.steps.length && req.type !== 'ApiKey'">
<ol class="api-key-steps" style="margin-top:8px">
<template x-for="step in req.install.steps" :key="step">
<li x-text="step"></li>
</template>
</ol>
</template>
<!-- API Key: numbered steps + signup link -->
<template x-if="req.type === 'ApiKey' && req.install">
<div>
<template x-if="req.install.steps && req.install.steps.length">
<ol class="api-key-steps">
<template x-for="step in req.install.steps" :key="step">
<li x-text="step"></li>
</template>
</ol>
</template>
<template x-if="req.install.env_example">
<div class="install-block" style="margin-top:8px">
<div class="install-cmd">
<code x-text="req.install.env_example"></code>
<button class="copy-btn" :class="{ copied: clipboardMsg === req.install.env_example }" @click="copyToClipboard(req.install.env_example)" x-text="clipboardMsg === req.install.env_example ? 'Copied!' : 'Copy'"></button>
</div>
</div>
</template>
<div class="flex gap-2 mt-2">
<template x-if="req.install.signup_url">
<a :href="req.install.signup_url" target="_blank" rel="noopener" class="btn btn-primary btn-sm">Get API Key &rarr;</a>
</template>
<template x-if="req.install.docs_url">
<a :href="req.install.docs_url" target="_blank" rel="noopener" class="btn btn-ghost btn-sm">Docs</a>
</template>
</div>
</div>
</template>
<!-- Manual download link -->
<template x-if="req.install && req.install.manual_url && req.type !== 'ApiKey'">
<div style="margin-top:6px">
<a :href="req.install.manual_url" target="_blank" rel="noopener" class="text-xs" style="color:var(--accent)">Manual download &rarr;</a>
</div>
</template>
</div>
</template>
</div>
</template>
<!-- Auto-Install Progress -->
<template x-if="installProgress">
<div class="install-progress-panel">
<div class="install-progress-header">
<template x-if="installProgress.status === 'installing'">
<div class="flex items-center gap-2">
<span class="spinner-sm"></span>
<span class="text-sm font-bold">Installing dependencies...</span>
</div>
</template>
<template x-if="installProgress.status === 'done' && !installProgress.error">
<div class="flex items-center gap-2">
<span style="color:var(--green);font-size:16px">&check;</span>
<span class="text-sm font-bold" style="color:var(--green)">Installation complete!</span>
</div>
</template>
<template x-if="installProgress.error">
<div class="flex items-center gap-2">
<span style="color:var(--red);font-size:16px">&cross;</span>
<span class="text-sm" style="color:var(--red)" x-text="installProgress.error"></span>
</div>
</template>
</div>
<!-- Per-dep results -->
<template x-if="installProgress.results.length > 0">
<div class="install-results-list">
<template x-for="r in installProgress.results" :key="r.key">
<div class="install-result-row" :class="getInstallResultClass(r.status)">
<span class="install-result-icon" x-text="getInstallResultIcon(r.status)"></span>
<span class="text-sm" x-text="r.key"></span>
<span class="text-xs text-dim" style="margin-left:auto" x-text="r.message"></span>
</div>
</template>
</div>
</template>
<!-- Installing spinner for current dep -->
<template x-if="installProgress.status === 'installing' && installProgress.results.length === 0">
<div class="install-results-list">
<template x-for="req in (setupWizard.requirements || []).filter(r => !r.satisfied)" :key="req.key">
<div class="install-result-row">
<span class="spinner-sm"></span>
<span class="text-sm" x-text="req.label"></span>
<span class="text-xs text-dim" style="margin-left:auto">Waiting...</span>
</div>
</template>
</div>
</template>
</div>
</template>
<!-- Progress bar -->
<div class="dep-progress">
<div class="dep-progress-label">
<span x-text="setupReqsMet + ' of ' + setupReqsTotal + ' ready'"></span>
<span class="text-dim" x-text="setupAllReqsMet ? 'All set!' : 'Install missing dependencies above'"></span>
</div>
<div class="dep-progress-bar">
<div class="dep-progress-fill" :style="'width:' + (setupReqsTotal ? Math.round(setupReqsMet / setupReqsTotal * 100) : 0) + '%'"></div>
</div>
</div>
</div>
<!-- ═══ Step 2: Configure ═══ -->
<div class="hand-wizard-body" x-show="setupStep === 2">
<template x-if="!setupHasSettings">
<div class="text-sm text-dim" style="text-align:center;padding:20px 0">No configuration needed for this hand. Click Next to continue.</div>
</template>
<template x-for="setting in (setupWizard.settings || [])" :key="setting.key">
<div class="mb-4">
<div class="text-xs text-dim mb-1" style="letter-spacing:0.5px;text-transform:uppercase" x-text="setting.label"></div>
<div class="text-xs text-dim mb-2" x-show="setting.description" x-text="setting.description"></div>
<!-- Select type: clickable option cards -->
<template x-if="setting.setting_type === 'select'">
<div style="display:flex;flex-direction:column;gap:6px">
<template x-for="opt in setting.options" :key="opt.value">
<div class="setting-option-card" :class="{ 'setting-option-selected': settingsValues[setting.key] === opt.value }" @click="selectOption(setting.key, opt.value)" style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border:1px solid var(--border);border-radius:6px;cursor:pointer;transition:all 0.15s">
<div>
<div class="text-sm" x-text="opt.label"></div>
<div class="text-xs text-dim" x-show="opt.provider_env" x-text="opt.provider_env"></div>
<div class="text-xs text-dim" x-show="opt.binary" x-text="'Requires: ' + opt.binary"></div>
</div>
<span class="badge" :class="opt.available ? 'badge-success' : 'badge-dim'" x-text="opt.available ? 'Ready' : 'Missing'" style="font-size:0.65rem"></span>
</div>
</template>
</div>
</template>
<!-- Toggle type -->
<template x-if="setting.setting_type === 'toggle'">
<label class="flex items-center gap-2" style="cursor:pointer">
<input type="checkbox" :checked="settingsValues[setting.key] === 'true'" @change="settingsValues[setting.key] = $event.target.checked ? 'true' : 'false'">
<span class="text-sm" x-text="settingsValues[setting.key] === 'true' ? 'Enabled' : 'Disabled'"></span>
</label>
</template>
<!-- Text type -->
<template x-if="setting.setting_type === 'text'">
<input type="text" class="input" x-model="settingsValues[setting.key]" :placeholder="setting.label" style="width:100%">
</template>
</div>
</template>
</div>
<!-- ═══ Step 3: Launch ═══ -->
<div class="hand-wizard-body" x-show="setupStep === 3">
<div class="launch-summary">
<div class="launch-summary-icon" x-text="setupWizard.icon"></div>
<div class="launch-summary-title" x-text="setupWizard.name"></div>
<div class="launch-summary-rows">
<!-- Dependencies summary -->
<template x-if="setupHasReqs">
<div class="launch-summary-row">
<span class="row-label">Dependencies</span>
<span class="row-check" x-text="setupReqsMet + '/' + setupReqsTotal + ' \u2713'"></span>
</div>
</template>
<!-- Settings summary -->
<template x-for="setting in (setupWizard.settings || [])" :key="setting.key">
<div class="launch-summary-row">
<span class="row-label" x-text="setting.label"></span>
<span class="row-value" x-text="getSettingDisplayValue(setting)"></span>
</div>
</template>
<!-- Provider / Model -->
<template x-if="setupWizard.agent">
<div class="launch-summary-row">
<span class="row-label">Model</span>
<span class="row-value" x-text="(setupWizard.agent.provider || 'default') + ' / ' + (setupWizard.agent.model || 'default')"></span>
</div>
</template>
</div>
</div>
</div>
<!-- Navigation -->
<div class="hand-wizard-nav">
<div>
<button class="btn btn-ghost" @click="closeSetupWizard()">Cancel</button>
<button class="btn btn-ghost" x-show="(setupStep === 2 && setupHasReqs) || setupStep === 3" @click="setupPrevStep()" style="margin-left:4px">Back</button>
</div>
<div class="flex gap-2">
<!-- Step 1: Install + Verify + Next -->
<template x-if="setupStep === 1">
<div class="flex gap-2">
<template x-if="!setupAllReqsMet">
<button class="btn btn-success" @click="installDeps()" :disabled="installProgress && installProgress.status === 'installing'" x-text="(installProgress && installProgress.status === 'installing') ? 'Installing...' : 'Install All'">
</button>
</template>
<button class="btn btn-ghost" @click="recheckDeps()" :disabled="setupChecking" x-text="setupChecking ? 'Checking...' : 'Verify'"></button>
<button class="btn btn-primary" @click="setupNextStep()" :disabled="!setupAllReqsMet">Next</button>
</div>
</template>
<!-- Step 2: Next -->
<template x-if="setupStep === 2">
<button class="btn btn-primary" @click="setupNextStep()">Next</button>
</template>
<!-- Step 3: Activate -->
<template x-if="setupStep === 3">
<button class="btn btn-success btn-launch" @click="launchHand()" :disabled="activatingId" x-text="activatingId ? 'Activating...' : 'Activate ' + setupWizard.name"></button>
</template>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Browser Viewer Modal -->
<template x-if="browserViewerOpen">
<div class="modal-overlay" @click.self="closeBrowserViewer()" @keydown.escape.window="closeBrowserViewer()">
<div class="browser-viewer">
<div class="browser-viewer-header">
<div class="browser-url-bar">
<span class="browser-dot red"></span>
<span class="browser-dot yellow"></span>
<span class="browser-dot green"></span>
<span class="browser-url" x-text="browserViewer ? (browserViewer.url || 'about:blank') : 'about:blank'"></span>
</div>
<button class="btn btn-ghost btn-sm" @click="refreshBrowserView()">Refresh</button>
<button class="btn btn-ghost btn-sm" @click="closeBrowserViewer()">Close</button>
</div>
<div class="browser-viewer-body">
<!-- Loading -->
<div x-show="browserViewer && browserViewer.loading" class="text-center text-dim" style="padding:40px">
Loading browser state...
</div>
<!-- Error -->
<div x-show="browserViewer && browserViewer.error && !browserViewer.loading" class="text-center" style="padding:40px;color:var(--error)">
<div style="font-size:2rem;margin-bottom:8px">!</div>
<span x-text="browserViewer ? browserViewer.error : ''"></span>
</div>
<!-- Screenshot -->
<div class="browser-screenshot" x-show="browserViewer && browserViewer.screenshot && !browserViewer.loading">
<img x-bind:src="browserViewer && browserViewer.screenshot ? ('data:image/png;base64,' + browserViewer.screenshot) : ''" alt="Browser screenshot" style="max-width:100%;border-radius:4px;display:block">
</div>
<!-- Page info -->
<div class="browser-info" x-show="browserViewer && !browserViewer.loading && !browserViewer.error">
<div class="text-dim text-sm">Title: <span x-text="browserViewer ? (browserViewer.title || '-') : '-'"></span></div>
</div>
</div>
</div>
</div>
</template>
<!-- Activation result toast -->
<div x-show="activateResult" x-transition class="info-card" style="position:fixed;bottom:24px;right:24px;z-index:200;max-width:360px" @click="activateResult = null">
<div class="flex items-center gap-2">
<span style="color:var(--success);font-size:1.2rem">\u2713</span>
<span x-text="activateResult"></span>
</div>
</div>
</div>
</div>
</template>
<!-- Page: Settings -->
<template x-if="page === 'settings'">
<div x-data="settingsPage">
<div class="page-header"><h2>Settings</h2></div>
<div class="page-body" x-init="loadSettings()">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading settings...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<div class="tabs" role="tablist">
<div class="tab" role="tab" :class="{ active: tab === 'providers' }" @click="tab = 'providers'">Providers</div>
<div class="tab" role="tab" :class="{ active: tab === 'models' }" @click="tab = 'models'">Models</div>
<div class="tab" role="tab" :class="{ active: tab === 'config' }" @click="tab = 'config'; if (!configSchema) loadConfigSchema()">Config</div>
<div class="tabs-separator"></div>
<div class="tab tab-secondary" role="tab" :class="{ active: tab === 'tools' }" @click="tab = 'tools'">Tools</div>
<div class="tab tab-secondary" role="tab" :class="{ active: tab === 'security' }" @click="tab = 'security'; if (!securityData && !secLoading) loadSecurity()">Security</div>
<div class="tab tab-secondary" role="tab" :class="{ active: tab === 'network' }" @click="tab = 'network'; if (!peers.length && !peersLoading) { loadPeers(); startPeerPolling(); }">Network</div>
<div class="tab tab-secondary" role="tab" :class="{ active: tab === 'budget' }" @click="tab = 'budget'">Budget</div>
<div class="tab tab-secondary" role="tab" :class="{ active: tab === 'info' }" @click="tab = 'info'">System</div>
<div class="tab tab-secondary" role="tab" :class="{ active: tab === 'migration' }" @click="tab = 'migration'">Migration</div>
</div>
<!-- Providers tab -->
<div x-show="tab === 'providers'">
<div class="info-card">
<h4>LLM Providers</h4>
<p>OpenFang supports 12 LLM providers out of the box. Configure API keys to unlock models from each provider. Set environment variables and restart, or use the form below to save keys directly.</p>
</div>
<div class="card-grid">
<template x-for="p in providers" :key="p.id">
<div class="card provider-card" :class="providerCardClass(p)">
<div class="flex justify-between items-center mb-2">
<div class="card-header" style="margin:0" x-text="p.display_name"></div>
<span class="badge" :class="providerAuthClass(p)" x-text="providerAuthText(p)"></span>
</div>
<div class="card-meta" x-text="(p.model_count || 0) + ' model(s) available'"></div>
<div class="text-xs text-dim mt-2" x-show="p.api_key_env" x-text="'Env: ' + p.api_key_env"></div>
<!-- Key input for unconfigured providers -->
<template x-if="p.auth_status !== 'configured' && p.api_key_env">
<div class="key-input-group">
<input type="password" :placeholder="'Enter ' + p.api_key_env" x-model="providerKeyInputs[p.id]">
<button class="btn btn-primary btn-sm" @click="saveProviderKey(p)">Save</button>
</div>
</template>
<template x-if="p.auth_status !== 'configured' && p.api_key_env">
<div class="text-xs text-dim mt-2">Or set <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px" x-text="p.api_key_env"></code> in your environment and restart</div>
</template>
<!-- Copilot OAuth button -->
<template x-if="p.id === 'github-copilot' && p.auth_status !== 'configured'">
<div class="mt-2">
<button class="btn btn-primary btn-sm" @click="startCopilotOAuth()" :disabled="copilotOAuth.polling" x-show="!copilotOAuth.userCode">Login with GitHub</button>
<div x-show="copilotOAuth.userCode" class="mt-2">
<div class="text-sm">Visit <a :href="copilotOAuth.verificationUri" target="_blank" x-text="copilotOAuth.verificationUri" style="color:var(--accent-light)"></a> and enter:</div>
<div style="font-size:24px;font-weight:bold;letter-spacing:4px;margin:8px 0;color:var(--accent-light)" x-text="copilotOAuth.userCode"></div>
<div class="text-xs text-dim"><span class="spinner" style="width:10px;height:10px;border-width:2px;display:inline-block;vertical-align:middle"></span> Waiting for authorization...</div>
</div>
</div>
</template>
<!-- Claude Code install hint -->
<template x-if="p.id === 'claude-code' && p.auth_status !== 'configured'">
<div class="mt-2 text-xs text-dim">Install: <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">npm install -g @anthropic-ai/claude-code</code></div>
</template>
<!-- Actions for configured providers -->
<template x-if="p.auth_status === 'configured'">
<div class="flex gap-2 mt-2">
<button class="btn btn-ghost btn-sm" @click="testProvider(p)" :disabled="providerTesting[p.id]">
<span x-show="!providerTesting[p.id]">Test</span>
<span x-show="providerTesting[p.id]" class="spinner" style="width:10px;height:10px;border-width:2px"></span>
</button>
<button class="btn btn-ghost btn-sm" style="color:var(--error)" @click="removeProviderKey(p)">Remove Key</button>
</div>
</template>
<!-- No key needed -->
<template x-if="!p.api_key_env || p.key_required === false">
<div class="text-xs mt-2" style="color:var(--success)" x-show="p.auth_status !== 'configured' && p.auth_status !== 'not_set' && p.auth_status !== 'missing'">No API key needed &mdash; runs locally or is free</div>
</template>
<!-- Base URL editor for local providers -->
<template x-if="p.is_local">
<div class="mt-3" style="border-top:1px solid var(--border);padding-top:8px">
<div class="text-xs text-dim mb-1">Base URL</div>
<div class="key-input-group">
<input type="text" :placeholder="'http://localhost:...'" x-model="providerUrlInputs[p.id]" style="font-size:12px">
<button class="btn btn-primary btn-sm" @click="saveProviderUrl(p)" :disabled="providerUrlSaving[p.id]">
<span x-show="!providerUrlSaving[p.id]">Save</span>
<span x-show="providerUrlSaving[p.id]" class="spinner" style="width:10px;height:10px;border-width:2px"></span>
</button>
</div>
</div>
</template>
</div>
</template>
</div>
<div class="empty-state" x-show="!providers.length">
<h4>No providers found</h4>
<p class="hint">Provider information could not be loaded. Check that the API is running.</p>
</div>
</div>
<!-- Models tab -->
<div x-show="tab === 'models'">
<div class="info-card">
<h4>Model Catalog</h4>
<p>Browse all available models across providers. Models marked "Available" have their provider configured and ready to use.</p>
</div>
<div class="flex gap-2 mb-4" style="flex-wrap:wrap">
<div class="search-input" style="flex:1;min-width:200px">
<span style="color:var(--text-muted)"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></span>
<input placeholder="Search models..." x-model="modelSearch">
</div>
<select class="form-select" style="width:160px" x-model="modelProviderFilter">
<option value="">All Providers</option>
<template x-for="pn in uniqueProviderNames" :key="pn">
<option :value="pn" x-text="pn"></option>
</template>
</select>
<select class="form-select" style="width:140px" x-model="modelTierFilter">
<option value="">All Tiers</option>
<template x-for="t in uniqueTiers" :key="t">
<option :value="t" x-text="t"></option>
</template>
</select>
<button class="btn btn-primary btn-sm" @click="showCustomModelForm = !showCustomModelForm" x-text="showCustomModelForm ? 'Cancel' : '+ Custom Model'"></button>
</div>
<!-- Custom model form -->
<div x-show="showCustomModelForm" class="info-card mb-4" style="border:1px solid var(--accent,#7c3aed)">
<h4 style="margin-top:0">Add Custom Model</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem">
<div>
<label class="text-xs text-dim">Model ID (required)</label>
<input class="form-input" x-model="customModelId" placeholder="e.g. my-org/my-model">
</div>
<div>
<label class="text-xs text-dim">Provider</label>
<input class="form-input" x-model="customModelProvider" placeholder="openrouter">
</div>
<div>
<label class="text-xs text-dim">Context Window</label>
<input class="form-input" type="number" x-model.number="customModelContext" placeholder="128000">
</div>
<div>
<label class="text-xs text-dim">Max Output Tokens</label>
<input class="form-input" type="number" x-model.number="customModelMaxOutput" placeholder="8192">
</div>
</div>
<button class="btn btn-primary btn-sm mt-2" @click="addCustomModel()" :disabled="!customModelId.trim()">Add Model</button>
<span class="text-xs text-dim ml-2" x-text="customModelStatus"></span>
</div>
<div class="text-xs text-dim mb-2" x-text="filteredModels.length + ' of ' + models.length + ' models'"></div>
<div class="table-wrap" x-show="filteredModels.length">
<table>
<thead><tr><th>Model</th><th>Provider</th><th>Tier</th><th>Context</th><th>Input Cost</th><th>Output Cost</th><th>Status</th></tr></thead>
<tbody>
<template x-for="m in filteredModels" :key="m.id">
<tr>
<td class="font-bold" style="font-size:11px" x-text="m.display_name || m.id"></td>
<td class="text-dim" x-text="m.provider"></td>
<td><span class="tier-badge" :class="tierBadgeClass(m.tier)" x-text="m.tier || '-'"></span></td>
<td class="text-xs" x-text="formatContext(m.context_window)"></td>
<td class="text-xs" x-text="formatCost(m.input_cost_per_m)"></td>
<td class="text-xs" x-text="formatCost(m.output_cost_per_m)"></td>
<td><span class="badge" :class="m.available ? 'badge-success' : 'badge-muted'" x-text="m.available ? 'Available' : 'Needs Key'"></span></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Tools tab -->
<div x-show="tab === 'tools'">
<div class="search-input mb-4">
<span style="color:var(--text-muted)"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></span>
<input placeholder="Search tools..." x-model="toolSearch">
</div>
<div class="text-xs text-dim mb-2" x-text="filteredTools.length + ' of ' + tools.length + ' tools'"></div>
<div class="table-wrap" x-show="filteredTools.length">
<table>
<thead><tr><th>Tool</th><th>Description</th></tr></thead>
<tbody>
<template x-for="t in filteredTools" :key="t.name">
<tr>
<td class="font-bold" style="white-space:nowrap" x-text="t.name"></td>
<td class="text-dim" x-text="t.description"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- System Info tab -->
<div x-show="tab === 'info'">
<div class="stats-row">
<div class="stat-card"><div class="stat-value" x-text="sysInfo.version || '-'"></div><div class="stat-label">Version</div></div>
<div class="stat-card"><div class="stat-value" x-text="sysInfo.platform || '-'"></div><div class="stat-label">Platform</div></div>
<div class="stat-card"><div class="stat-value" x-text="formatUptime(sysInfo.uptime_seconds)"></div><div class="stat-label">Uptime</div></div>
<div class="stat-card"><div class="stat-value" x-text="sysInfo.agent_count || 0"></div><div class="stat-label">Agents</div></div>
</div>
<div class="card mt-4" x-show="sysInfo.default_provider">
<div class="card-header">Default Model</div>
<div class="card-meta" x-text="sysInfo.default_provider + ' : ' + sysInfo.default_model"></div>
</div>
</div>
<!-- Config tab -->
<div x-show="tab === 'config'">
<div class="info-card">
<h4>Runtime Configuration</h4>
<p>View and edit the active configuration. Changes are applied immediately. For advanced edits, modify <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">config.toml</code> in your OpenFang data directory.</p>
</div>
<!-- Dynamic config form (when schema is available) -->
<template x-if="configSchema">
<div>
<template x-for="(fields, section) in configSchema" :key="section">
<div class="card mb-4">
<div class="card-header" style="text-transform:capitalize" x-text="section.replace(/_/g, ' ')"></div>
<div class="detail-grid" style="margin-top:12px">
<template x-for="field in fields" :key="section + '.' + field.name">
<div class="detail-row" style="align-items:center">
<span class="detail-label" x-text="field.label || field.name"></span>
<div style="display:flex;align-items:center;gap:8px;flex:1;min-width:0">
<template x-if="field.type === 'boolean'">
<label class="form-checkbox" style="margin:0">
<input type="checkbox"
:checked="configValues[section] && configValues[section][field.name]"
@change="configValues[section] = configValues[section] || {}; configValues[section][field.name] = $event.target.checked; markConfigDirty(section, field.name)">
</label>
</template>
<template x-if="field.type === 'number'">
<input class="form-input" type="number" style="width:120px"
:value="configValues[section] && configValues[section][field.name]"
@input="configValues[section] = configValues[section] || {}; configValues[section][field.name] = Number($event.target.value); markConfigDirty(section, field.name)">
</template>
<template x-if="field.type === 'select' && field.options">
<select class="form-select" style="width:180px"
:value="configValues[section] && configValues[section][field.name]"
@change="configValues[section] = configValues[section] || {}; configValues[section][field.name] = $event.target.value; markConfigDirty(section, field.name)">
<template x-for="opt in field.options" :key="opt">
<option :value="opt" x-text="opt" :selected="configValues[section] && configValues[section][field.name] === opt"></option>
</template>
</select>
</template>
<template x-if="!field.type || field.type === 'string'">
<input class="form-input" type="text" style="flex:1"
:value="configValues[section] && configValues[section][field.name]"
@input="configValues[section] = configValues[section] || {}; configValues[section][field.name] = $event.target.value; markConfigDirty(section, field.name)">
</template>
<button class="btn btn-primary btn-sm" x-show="isConfigDirty(section, field.name)"
@click="saveConfigField(section, field.name, configValues[section] && configValues[section][field.name])"
:disabled="configSaving[section + '.' + field.name]">
<span x-show="!configSaving[section + '.' + field.name]">Save</span>
<span x-show="configSaving[section + '.' + field.name]" class="spinner" style="width:12px;height:12px;border-width:2px"></span>
</button>
</div>
<div class="text-xs text-dim" x-show="field.description" x-text="field.description" style="grid-column:1/-1;padding-left:2px"></div>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
<!-- Raw JSON fallback -->
<div class="card mt-4" style="border-left:3px solid var(--border)">
<div class="card-header" style="font-size:12px;cursor:pointer" @click="$el.nextElementSibling.style.display = $el.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Raw Config JSON (click to toggle)</div>
<pre style="font-size:11px;white-space:pre-wrap;overflow:auto;max-height:400px;color:var(--text-dim);line-height:1.5;display:none" x-text="JSON.stringify(config, null, 2)"></pre>
</div>
</div>
<!-- Security tab -->
<div x-show="tab === 'security'">
<div x-show="secLoading" class="loading-state"><div class="spinner"></div><span>Loading security data...</span></div>
<div x-show="!secLoading">
<div class="security-hero">
<div class="security-hero-shield"><svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div>
<div>
<div class="security-hero-title">Defense in Depth</div>
<div class="security-hero-desc">OpenFang implements 15 layered security features across the entire stack &mdash; from network ingress to agent sandboxing to cryptographic audit trails. Core protections cannot be disabled.</div>
</div>
</div>
<div class="security-section">
<div class="security-section-header">
<span class="security-shield shield-core"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></span>
<div>
<div class="font-bold">Core Protections</div>
<div class="text-xs text-dim">Always active. Cannot be disabled.</div>
</div>
</div>
<div class="security-grid">
<template x-for="f in coreFeatures" :key="f.key">
<div class="security-card">
<div class="flex justify-between items-center mb-2">
<div class="security-card-name" x-text="f.name"></div>
<span class="sec-badge sec-badge-core">ALWAYS ON</span>
</div>
<div class="security-card-desc" x-text="f.description"></div>
<div class="security-card-threat">
<span class="text-xs font-bold" style="color:var(--error)">Protects against:</span>
<span class="text-xs" x-text="f.threat"></span>
</div>
<div class="text-xs text-dim" style="margin-top:6px;opacity:0.5" x-text="f.impl"></div>
</div>
</template>
</div>
</div>
<div class="security-section">
<div class="security-section-header">
<span class="security-shield shield-config"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/></svg></span>
<div>
<div class="font-bold">Configurable Controls</div>
<div class="text-xs text-dim">Active with tunable parameters.</div>
</div>
</div>
<div class="security-grid">
<template x-for="f in configurableFeatures" :key="f.key">
<div class="security-card">
<div class="flex justify-between items-center mb-2">
<div class="security-card-name" x-text="f.name"></div>
<span class="sec-badge sec-badge-config">CONFIGURABLE</span>
</div>
<div class="security-card-desc" x-text="f.description"></div>
<div class="security-card-value" x-text="formatConfigValue(f)"></div>
</div>
</template>
</div>
</div>
<div class="security-section">
<div class="security-section-header">
<span class="security-shield shield-monitor"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/><path d="m9 14 2 2 4-4"/></svg></span>
<div>
<div class="font-bold">Monitoring &amp; Analysis</div>
<div class="text-xs text-dim">Active monitoring systems.</div>
</div>
</div>
<div class="security-grid">
<template x-for="f in monitoringFeatures" :key="f.key">
<div class="security-card">
<div class="flex justify-between items-center mb-2">
<div class="security-card-name" x-text="f.name"></div>
<span class="sec-badge sec-badge-monitor">MONITORING</span>
</div>
<div class="security-card-desc" x-text="f.description"></div>
<div class="security-card-value" x-text="formatMonitoringValue(f)"></div>
</div>
</template>
<div class="security-card" style="border-color:var(--accent)">
<div class="flex justify-between items-center mb-2">
<div class="security-card-name">Audit Chain Integrity</div>
<button class="btn btn-ghost btn-sm" @click="verifyAuditChain()" :disabled="verifyingChain">
<span x-show="!verifyingChain">Verify Now</span>
<span x-show="verifyingChain" class="spinner" style="width:12px;height:12px;border-width:2px"></span>
</button>
</div>
<div class="security-card-desc">Run cryptographic verification of the entire SHA-256 Merkle hash chain.</div>
<div x-show="chainResult" class="mt-2">
<span class="sec-badge" :class="chainResult && chainResult.valid ? 'sec-badge-core' : 'sec-badge-warn'" x-text="chainResult && chainResult.valid ? 'CHAIN VALID — ' + (chainResult.entries || 0) + ' entries verified' : 'CHAIN BROKEN — ' + (chainResult ? chainResult.error : '')"></span>
</div>
</div>
</div>
</div>
<div class="card mt-4" style="border-left:3px solid var(--accent)">
<div class="font-bold" style="font-size:13px;margin-bottom:6px">Security Dependencies</div>
<div class="text-xs text-dim" style="line-height:1.8">
<code style="color:var(--accent-light)">sha2</code> SHA-256 &middot;
<code style="color:var(--accent-light)">hmac</code> HMAC-SHA256 &middot;
<code style="color:var(--accent-light)">subtle</code> constant-time &middot;
<code style="color:var(--accent-light)">ed25519-dalek</code> signing &middot;
<code style="color:var(--accent-light)">zeroize</code> secret wiping &middot;
<code style="color:var(--accent-light)">rand</code> randomness &middot;
<code style="color:var(--accent-light)">governor</code> rate limiting
</div>
</div>
</div>
</div>
<!-- Network tab -->
<div x-show="tab === 'network'" x-data="{
netStatus: null, a2aAgents: [], a2aDiscoverUrl: '', a2aDiscovering: false,
async loadNetStatus() { try { this.netStatus = await (await fetch('/api/network/status')).json(); } catch(e) {} },
async loadA2aAgents() { try { let r = await (await fetch('/api/a2a/agents')).json(); this.a2aAgents = r.agents || []; } catch(e) {} },
async discoverA2a() {
if (!this.a2aDiscoverUrl) return; this.a2aDiscovering = true;
try { await fetch('/api/a2a/discover', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:this.a2aDiscoverUrl})}); this.a2aDiscoverUrl=''; await this.loadA2aAgents(); } catch(e) {}
this.a2aDiscovering = false;
}
}" x-init="loadNetStatus(); loadA2aAgents()">
<!-- OFP Status -->
<div class="info-card" style="display:flex;justify-content:space-between;align-items:center">
<div>
<h4>Peer Networking (OFP)</h4>
<p>Link multiple OpenFang instances into a mesh via the OFP wire protocol.</p>
</div>
<span class="badge" :class="netStatus && netStatus.enabled ? 'badge-success' : 'badge-muted'" x-text="netStatus && netStatus.enabled ? 'Enabled' : 'Disabled'" style="font-size:12px;padding:4px 10px"></span>
</div>
<div x-show="netStatus && netStatus.enabled" class="text-xs text-dim mb-2" style="margin-top:-8px">
Node: <span x-text="netStatus?.node_id?.substring(0,8) + '...'" class="font-bold"></span> &bull;
Listening on <span x-text="netStatus?.listen_address" class="font-bold"></span>
</div>
<div x-show="peersLoading" class="loading-state"><div class="spinner"></div><span>Loading peers...</span></div>
<div x-show="!peersLoading && peersLoadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="peersLoadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadPeers()">Retry</button>
</div>
<div x-show="!peersLoading && !peersLoadError">
<div class="stats-row">
<div class="stat-card"><div class="stat-value" x-text="peers.filter(function(p){return p.state === 'Connected'}).length"></div><div class="stat-label">Connected</div></div>
<div class="stat-card"><div class="stat-value" x-text="peers.length"></div><div class="stat-label">Total Peers</div></div>
</div>
<div class="table-wrap" x-show="peers.length">
<table>
<thead><tr><th>Node</th><th>Address</th><th>State</th><th>Agents</th><th>Protocol</th></tr></thead>
<tbody>
<template x-for="p in peers" :key="p.node_id">
<tr>
<td><span class="font-bold" x-text="p.node_name"></span><br><span class="text-xs text-dim" x-text="p.node_id"></span></td>
<td x-text="p.address"></td>
<td><span class="badge" :class="p.state === 'Connected' ? 'badge-connected' : 'badge-disconnected'" x-text="p.state"></span></td>
<td x-text="(p.agents || []).length"></td>
<td x-text="'v' + p.protocol_version"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!peers.length">
<h4>No peers connected</h4>
<p class="hint">Add a <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">[network]</code> section to config.toml with <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">shared_secret</code> and peer addresses.</p>
</div>
</div>
<!-- A2A External Agents -->
<div style="margin-top:24px;border-top:1px solid var(--border);padding-top:16px">
<h4 style="margin-bottom:8px">A2A External Agents</h4>
<p class="text-xs text-dim mb-2">Discovered agents on other OpenFang/A2A-compatible instances that this node can communicate with.</p>
<div class="flex gap-2 mb-3">
<input class="form-input" style="flex:1" placeholder="https://remote-agent.example.com" x-model="a2aDiscoverUrl">
<button class="btn btn-primary btn-sm" @click="discoverA2a()" :disabled="a2aDiscovering">
<span x-show="!a2aDiscovering">Discover</span>
<span x-show="a2aDiscovering">...</span>
</button>
</div>
<div class="table-wrap" x-show="a2aAgents.length">
<table>
<thead><tr><th>Name</th><th>URL</th><th>Description</th></tr></thead>
<tbody>
<template x-for="a in a2aAgents" :key="a.url">
<tr>
<td class="font-bold" x-text="a.name"></td>
<td class="text-xs text-dim" x-text="a.url"></td>
<td class="text-xs" x-text="(a.description || '').substring(0,80)"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="text-xs text-dim" x-show="!a2aAgents.length">No external agents discovered yet. Enter a URL above to discover one.</div>
</div>
</div>
<!-- Budget tab -->
<div x-show="tab === 'budget'" x-data="{
budgetData: null, agentRanking: [], budgetLoading: true,
editMode: false,
editHourly: '', editDaily: '', editMonthly: '', editAlert: '',
saving: false,
async loadBudget() {
this.budgetLoading = true;
try {
let [b, a] = await Promise.all([
fetch('/api/budget').then(r => r.json()),
fetch('/api/budget/agents').then(r => r.json())
]);
this.budgetData = b;
this.agentRanking = a.agents || [];
} catch(e) {}
this.budgetLoading = false;
},
startEdit() {
this.editHourly = this.budgetData.hourly_limit || 0;
this.editDaily = this.budgetData.daily_limit || 0;
this.editMonthly = this.budgetData.monthly_limit || 0;
this.editAlert = ((this.budgetData.alert_threshold || 0.8) * 100).toFixed(0);
this.editMode = true;
},
async saveBudget() {
this.saving = true;
try {
let body = {};
if (+this.editHourly !== this.budgetData.hourly_limit) body.max_hourly_usd = +this.editHourly;
if (+this.editDaily !== this.budgetData.daily_limit) body.max_daily_usd = +this.editDaily;
if (+this.editMonthly !== this.budgetData.monthly_limit) body.max_monthly_usd = +this.editMonthly;
let alertVal = (+this.editAlert) / 100;
if (Math.abs(alertVal - this.budgetData.alert_threshold) > 0.001) body.alert_threshold = alertVal;
await fetch('/api/budget', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
this.editMode = false;
await this.loadBudget();
} catch(e) { alert('Failed to save: ' + e); }
this.saving = false;
},
pctColor(pct) { return pct >= 0.8 ? '#ef4444' : pct >= 0.5 ? '#eab308' : '#22c55e'; },
fmtUsd(v) { return v > 0 ? '$' + v.toFixed(4) : 'unlimited'; }
}" x-init="loadBudget()">
<div class="info-card" style="display:flex;justify-content:space-between;align-items:start">
<div>
<h4>Budget & Spending Limits</h4>
<p>Monitor and control spending across all agents.</p>
</div>
<button class="btn btn-sm" @click="editMode ? saveBudget() : startEdit()" :disabled="saving" x-text="editMode ? (saving ? 'Saving...' : 'Save') : 'Edit Limits'" style="white-space:nowrap"></button>
</div>
<div x-show="budgetLoading" class="loading-state"><div class="spinner"></div><span>Loading budget...</span></div>
<div x-show="!budgetLoading && budgetData">
<!-- Global budget meters -->
<div class="stats-row" style="margin-bottom:16px">
<div class="stat-card" style="flex:1">
<div class="stat-label">Hourly</div>
<div class="stat-value" style="font-size:18px" x-text="'$' + (budgetData.hourly_spend || 0).toFixed(4)"></div>
<div class="text-xs text-dim" x-text="'of ' + fmtUsd(budgetData.hourly_limit)"></div>
<div style="height:4px;background:var(--border);border-radius:2px;margin-top:4px;overflow:hidden" x-show="budgetData.hourly_limit > 0">
<div style="height:100%;border-radius:2px;transition:width 0.3s" :style="{width: Math.min(budgetData.hourly_pct*100,100)+'%', background: pctColor(budgetData.hourly_pct)}"></div>
</div>
</div>
<div class="stat-card" style="flex:1">
<div class="stat-label">Daily</div>
<div class="stat-value" style="font-size:18px" x-text="'$' + (budgetData.daily_spend || 0).toFixed(4)"></div>
<div class="text-xs text-dim" x-text="'of ' + fmtUsd(budgetData.daily_limit)"></div>
<div style="height:4px;background:var(--border);border-radius:2px;margin-top:4px;overflow:hidden" x-show="budgetData.daily_limit > 0">
<div style="height:100%;border-radius:2px;transition:width 0.3s" :style="{width: Math.min(budgetData.daily_pct*100,100)+'%', background: pctColor(budgetData.daily_pct)}"></div>
</div>
</div>
<div class="stat-card" style="flex:1">
<div class="stat-label">Monthly</div>
<div class="stat-value" style="font-size:18px" x-text="'$' + (budgetData.monthly_spend || 0).toFixed(4)"></div>
<div class="text-xs text-dim" x-text="'of ' + fmtUsd(budgetData.monthly_limit)"></div>
<div style="height:4px;background:var(--border);border-radius:2px;margin-top:4px;overflow:hidden" x-show="budgetData.monthly_limit > 0">
<div style="height:100%;border-radius:2px;transition:width 0.3s" :style="{width: Math.min(budgetData.monthly_pct*100,100)+'%', background: pctColor(budgetData.monthly_pct)}"></div>
</div>
</div>
</div>
<div class="text-xs text-dim mb-3" x-show="budgetData.alert_threshold > 0 && !editMode">
Alert threshold: <span x-text="(budgetData.alert_threshold * 100).toFixed(0) + '%'"></span> of any limit
</div>
<!-- Edit limits form -->
<div x-show="editMode" class="card" style="margin:12px 0;padding:12px;border:1px solid var(--accent);border-radius:6px">
<div class="stats-row" style="margin-bottom:8px;gap:8px">
<div style="flex:1">
<label class="text-xs text-dim">Hourly Limit ($)</label>
<input type="number" step="0.1" min="0" x-model="editHourly" class="input" style="width:100%;margin-top:2px" placeholder="0 = unlimited">
</div>
<div style="flex:1">
<label class="text-xs text-dim">Daily Limit ($)</label>
<input type="number" step="1" min="0" x-model="editDaily" class="input" style="width:100%;margin-top:2px" placeholder="0 = unlimited">
</div>
<div style="flex:1">
<label class="text-xs text-dim">Monthly Limit ($)</label>
<input type="number" step="1" min="0" x-model="editMonthly" class="input" style="width:100%;margin-top:2px" placeholder="0 = unlimited">
</div>
<div style="flex:0.6">
<label class="text-xs text-dim">Alert (%)</label>
<input type="number" step="5" min="0" max="100" x-model="editAlert" class="input" style="width:100%;margin-top:2px" placeholder="80">
</div>
</div>
<div class="text-xs text-dim">Set to 0 for unlimited. Changes apply immediately (in-memory, not persisted to config.toml).</div>
<button class="btn btn-sm mt-2" @click="editMode = false" style="margin-right:8px">Cancel</button>
</div>
<!-- Per-agent cost ranking -->
<h4 style="margin-top:16px;margin-bottom:8px">Top Spenders (Today)</h4>
<div class="table-wrap" x-show="agentRanking.length">
<table>
<thead><tr><th>Agent</th><th>Today</th><th>Hourly Limit</th><th>Daily Limit</th><th>Monthly Limit</th></tr></thead>
<tbody>
<template x-for="a in agentRanking" :key="a.agent_id">
<tr>
<td class="font-bold" x-text="a.name"></td>
<td x-text="'$' + (a.daily_cost_usd || 0).toFixed(4)"></td>
<td class="text-dim" x-text="fmtUsd(a.hourly_limit)"></td>
<td class="text-dim" x-text="fmtUsd(a.daily_limit)"></td>
<td class="text-dim" x-text="fmtUsd(a.monthly_limit)"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="text-xs text-dim" x-show="!agentRanking.length">No spending recorded today.</div>
</div>
</div>
<!-- Migration tab -->
<div x-show="tab === 'migration'">
<div class="card mb-4" style="border-left:3px solid var(--accent)" x-show="migStep === 'intro'">
<div class="font-bold" style="font-size:14px;margin-bottom:6px">Migrate from OpenClaw</div>
<div class="text-sm text-dim" style="line-height:1.7">
Seamlessly transfer your agents, memory, workspace files, and channel configurations from OpenClaw to OpenFang.
</div>
<ul class="text-sm text-dim" style="margin:8px 0 0 16px;line-height:1.8">
<li>Converts agent.yaml to agent.toml with proper capabilities</li>
<li>Maps tools (read_file &rarr; file_read, execute_command &rarr; shell_exec, etc.)</li>
<li>Merges channel configs into config.toml</li>
<li>Copies workspace files and memory data</li>
</ul>
<div class="flex gap-2 mt-4">
<button class="btn btn-primary" @click="autoDetect()" :disabled="detecting">
<span x-show="!detecting">Auto-Detect OpenClaw</span>
<span x-show="detecting">Scanning...</span>
</button>
<button class="btn btn-ghost" @click="migStep = 'manual'">Enter Path Manually</button>
</div>
</div>
<div class="card mb-4" x-show="migStep === 'manual'" style="border-left:3px solid var(--accent)">
<div class="font-bold" style="font-size:14px;margin-bottom:8px">Specify OpenClaw Path</div>
<div class="form-group">
<label>OpenClaw Home Directory</label>
<input class="form-input" type="text" x-model="sourcePath" placeholder="~/.openclaw" style="font-family:monospace;font-size:12px">
</div>
<div class="form-group mt-2">
<label>OpenFang Target Directory</label>
<input class="form-input" type="text" x-model="targetPath" placeholder="~/.openfang (default)" style="font-family:monospace;font-size:12px">
</div>
<div class="flex gap-2 mt-4">
<button class="btn btn-primary" @click="scanPath()" :disabled="!sourcePath || scanning">
<span x-show="!scanning">Scan Directory</span>
<span x-show="scanning">Scanning...</span>
</button>
<button class="btn btn-ghost" @click="migStep = 'intro'">Back</button>
</div>
</div>
<div x-show="migStep === 'preview' && scanResult">
<div class="card mb-4" style="border-left:3px solid var(--success, #22c55e)">
<div class="flex justify-between items-center mb-2">
<div class="font-bold" style="font-size:14px">OpenClaw Workspace Found</div>
<span class="badge badge-connected">Ready to Migrate</span>
</div>
<div class="text-sm text-dim" style="font-family:monospace" x-text="scanResult.path"></div>
</div>
<div class="stats-row mb-4">
<div class="stat-card"><div class="stat-value" x-text="scanResult.agents ? scanResult.agents.length : 0"></div><div class="stat-label">Agents</div></div>
<div class="stat-card"><div class="stat-value" x-text="scanResult.channels ? scanResult.channels.length : 0"></div><div class="stat-label">Channels</div></div>
<div class="stat-card"><div class="stat-value" x-text="scanResult.skills ? scanResult.skills.length : 0"></div><div class="stat-label">Skills</div></div>
</div>
<div class="flex gap-2 mb-4">
<button class="btn btn-primary" @click="runMigration(false)" :disabled="migrating">
<span x-show="!migrating">Migrate Now</span>
<span x-show="migrating">Migrating...</span>
</button>
<button class="btn btn-ghost" @click="runMigration(true)" :disabled="migrating">Dry Run</button>
<button class="btn btn-ghost" @click="migStep = 'intro'; scanResult = null">Start Over</button>
</div>
</div>
<div x-show="migStep === 'result' && migResult">
<div class="card mb-4" :style="'border-left:3px solid ' + (migResult.status === 'completed' ? 'var(--success, #22c55e)' : 'var(--error)')">
<div class="flex justify-between items-center mb-2">
<div class="font-bold" style="font-size:14px" x-text="migResult.dry_run ? 'Dry Run Complete' : 'Migration Complete!'"></div>
<span class="badge" :class="migResult.status === 'completed' ? 'badge-connected' : 'badge-crashed'" x-text="migResult.status === 'completed' ? 'SUCCESS' : 'FAILED'"></span>
</div>
<div class="text-sm text-dim" x-show="migResult.error" style="color:var(--error)" x-text="migResult.error"></div>
</div>
<div class="flex gap-2">
<button class="btn btn-primary" x-show="migResult.dry_run" @click="runMigration(false)" :disabled="migrating">Run Migration for Real</button>
<button class="btn btn-ghost" @click="migStep = 'intro'; migResult = null; scanResult = null">Start New Migration</button>
</div>
</div>
<div class="card" x-show="migStep === 'not_found'" style="border-left:3px solid var(--warning, #f59e0b)">
<div class="font-bold mb-2" style="font-size:14px">OpenClaw Not Found</div>
<div class="text-sm text-dim">Could not auto-detect an OpenClaw installation.</div>
<div class="flex gap-2 mt-4">
<button class="btn btn-primary" @click="migStep = 'manual'">Enter Path Manually</button>
<button class="btn btn-ghost" @click="migStep = 'intro'">Back</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Page: Analytics -->
<template x-if="page === 'analytics'">
<div x-data="analyticsPage">
<div class="page-header"><h2>Analytics</h2></div>
<div class="page-body" x-init="loadUsage()">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading usage data...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<div class="stats-row">
<div class="stat-card"><div class="stat-value" x-text="formatTokens((summary.total_input_tokens || 0) + (summary.total_output_tokens || 0))"></div><div class="stat-label">Total Tokens</div></div>
<div class="stat-card"><div class="stat-value" x-text="formatCost(summary.total_cost_usd)"></div><div class="stat-label">Estimated Cost</div></div>
<div class="stat-card"><div class="stat-value" x-text="summary.call_count || 0"></div><div class="stat-label">API Calls</div></div>
<div class="stat-card"><div class="stat-value" x-text="summary.total_tool_calls || 0"></div><div class="stat-label">Tool Calls</div></div>
</div>
<div class="tabs mt-4">
<div class="tab" :class="{ active: tab === 'summary' }" @click="tab = 'summary'">Summary</div>
<div class="tab" :class="{ active: tab === 'by-model' }" @click="tab = 'by-model'">By Model</div>
<div class="tab" :class="{ active: tab === 'by-agent' }" @click="tab = 'by-agent'">By Agent</div>
<div class="tab" :class="{ active: tab === 'costs' }" @click="tab = 'costs'">Costs</div>
</div>
<div x-show="tab === 'summary'">
<div class="card mt-4">
<div class="card-header">Token Breakdown</div>
<div class="detail-grid" style="margin-top:8px">
<div class="detail-row"><span class="detail-label">Input Tokens</span><span class="detail-value" x-text="formatTokens(summary.total_input_tokens)"></span></div>
<div class="detail-row"><span class="detail-label">Output Tokens</span><span class="detail-value" x-text="formatTokens(summary.total_output_tokens)"></span></div>
<div class="detail-row"><span class="detail-label">Total Cost</span><span class="detail-value" x-text="formatCost(summary.total_cost_usd)"></span></div>
<div class="detail-row"><span class="detail-label">API Calls</span><span class="detail-value" x-text="summary.call_count || 0"></span></div>
<div class="detail-row"><span class="detail-label">Tool Calls</span><span class="detail-value" x-text="summary.total_tool_calls || 0"></span></div>
</div>
</div>
</div>
<div x-show="tab === 'by-model'">
<div class="table-wrap mt-4" x-show="byModel.length">
<table>
<thead><tr><th>Model</th><th>Calls</th><th>Input Tokens</th><th>Output Tokens</th><th>Cost</th><th style="width:30%">Usage</th></tr></thead>
<tbody>
<template x-for="m in byModel" :key="m.model">
<tr>
<td class="font-bold" style="font-size:11px" x-text="m.model"></td>
<td x-text="m.call_count"></td>
<td x-text="formatTokens(m.total_input_tokens)"></td>
<td x-text="formatTokens(m.total_output_tokens)"></td>
<td x-text="formatCost(m.total_cost_usd)"></td>
<td><div style="background:var(--surface2);border-radius:4px;height:16px;overflow:hidden"><div style="height:100%;border-radius:4px;background:var(--accent);transition:width 0.3s" :style="'width:' + barWidth(m)"></div></div></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!byModel.length"><p>No model usage data yet.</p></div>
</div>
<div x-show="tab === 'by-agent'">
<div class="table-wrap mt-4" x-show="byAgent.length">
<table>
<thead><tr><th>Agent</th><th>Total Tokens</th><th>Tool Calls</th></tr></thead>
<tbody>
<template x-for="a in byAgent" :key="a.agent_id">
<tr>
<td class="font-bold" x-text="a.name"></td>
<td x-text="a.total_tokens ? a.total_tokens.toLocaleString() : '0'"></td>
<td x-text="a.tool_calls || 0"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!byAgent.length"><p>No agent usage data yet.</p></div>
</div>
<!-- Costs Tab -->
<div x-show="tab === 'costs'">
<!-- Cost Summary Cards -->
<div class="stats-row mt-4">
<div class="stat-card">
<div class="stat-value" x-text="formatCost(summary.total_cost_usd)"></div>
<div class="stat-label">Total Spend</div>
</div>
<div class="stat-card">
<div class="stat-value" x-text="formatCost(todayCost)"></div>
<div class="stat-label">Today's Spend</div>
</div>
<div class="stat-card">
<div class="stat-value" x-text="formatCost(projectedMonthlyCost())"></div>
<div class="stat-label">Projected Monthly</div>
</div>
<div class="stat-card">
<div class="stat-value" x-text="formatCost(avgCostPerMessage())"></div>
<div class="stat-label">Avg Cost / Message</div>
</div>
</div>
<!-- Charts Row: Donut + Bar side by side -->
<div class="cost-charts-row mt-4">
<!-- Donut Chart: Cost by Provider -->
<div class="card cost-chart-panel">
<div class="card-header">Cost by Provider</div>
<div x-show="costByProvider().length === 0" class="text-sm text-dim" style="padding:20px;text-align:center">No cost data yet.</div>
<div x-show="costByProvider().length > 0" class="donut-chart-wrap">
<div class="donut-chart">
<svg viewBox="0 0 160 160" width="160" height="160">
<template x-for="(seg, idx) in donutSegments()" :key="seg.provider">
<circle
cx="80" cy="80" r="60"
fill="none"
:stroke="seg.color"
stroke-width="24"
:stroke-dasharray="seg.dasharray"
:stroke-dashoffset="seg.dashoffset"
transform="rotate(-90 80 80)"
class="donut-segment"
>
<title x-text="seg.provider + ': ' + seg.percent + '% (' + formatCost(seg.cost) + ')'"></title>
</circle>
</template>
<!-- Center text -->
<text x="80" y="76" text-anchor="middle" fill="var(--text)" style="font-size:14px;font-weight:700;font-family:var(--font-mono)" x-text="formatCost(summary.total_cost_usd)"></text>
<text x="80" y="92" text-anchor="middle" fill="var(--text-muted)" style="font-size:9px;font-family:var(--font-mono)">TOTAL</text>
</svg>
</div>
<div class="donut-legend">
<template x-for="(seg, idx) in donutSegments()" :key="'legend-' + seg.provider">
<div class="donut-legend-item">
<span class="donut-legend-swatch" :style="'background:' + seg.color"></span>
<span class="donut-legend-label" x-text="seg.provider"></span>
<span class="donut-legend-pct" x-text="seg.percent + '%'"></span>
<span class="donut-legend-cost text-dim" x-text="formatCost(seg.cost)"></span>
</div>
</template>
</div>
</div>
</div>
<!-- Bar Chart: Daily Cost (last 7 days) -->
<div class="card cost-chart-panel">
<div class="card-header">Daily Cost (Last 7 Days)</div>
<div x-show="barChartData().length === 0" class="text-sm text-dim" style="padding:20px;text-align:center">No daily data yet.</div>
<div x-show="barChartData().length > 0" class="bar-chart">
<svg :viewBox="'0 0 ' + (barChartData().length * 50 + 20) + ' 180'" :width="barChartData().length * 50 + 20" height="180">
<!-- Baseline -->
<line x1="10" :x2="barChartData().length * 50 + 10" y1="150" y2="150" stroke="var(--border)" stroke-width="1"/>
<template x-for="(bar, idx) in barChartData()" :key="bar.date">
<g>
<!-- Bar rect -->
<rect
:x="idx * 50 + 18"
:y="150 - bar.barHeight"
width="24"
:height="bar.barHeight"
rx="3"
fill="var(--accent)"
class="cost-bar"
style="opacity:0.85"
>
<title x-text="bar.date + ': ' + formatCost(bar.cost) + ' (' + bar.calls + ' calls)'"></title>
</rect>
<!-- Day label -->
<text
:x="idx * 50 + 30"
y="166"
text-anchor="middle"
fill="var(--text-muted)"
style="font-size:9px;font-family:var(--font-mono)"
x-text="bar.dayName"
></text>
<!-- Cost label on top -->
<text
:x="idx * 50 + 30"
:y="150 - bar.barHeight - 4"
text-anchor="middle"
fill="var(--text-dim)"
style="font-size:8px;font-family:var(--font-mono)"
x-text="formatCost(bar.cost)"
></text>
</g>
</template>
</svg>
</div>
</div>
</div>
<!-- Cost by Model Table -->
<div class="card mt-4">
<div class="card-header">Cost by Model</div>
<div class="table-wrap" x-show="costByModelSorted().length" style="border:none;margin-top:8px">
<table>
<thead>
<tr>
<th>Model</th>
<th>Provider</th>
<th>Tier</th>
<th>Input Tokens</th>
<th>Output Tokens</th>
<th>Calls</th>
<th>Cost</th>
<th style="width:20%">Cost Share</th>
</tr>
</thead>
<tbody>
<template x-for="m in costByModelSorted()" :key="'cost-' + m.model">
<tr>
<td class="font-bold" style="font-size:11px" x-text="m.model"></td>
<td><span class="badge badge-muted" style="font-size:9px" x-text="_extractProvider(m.model)"></span></td>
<td>
<span class="tier-badge"
:class="'tier-' + modelTier(m.model)"
x-text="modelTier(m.model)"></span>
</td>
<td x-text="formatTokens(m.total_input_tokens)"></td>
<td x-text="formatTokens(m.total_output_tokens)"></td>
<td x-text="m.call_count"></td>
<td class="font-bold" x-text="formatCost(m.total_cost_usd)"></td>
<td>
<div style="background:var(--surface2);border-radius:4px;height:16px;overflow:hidden">
<div style="height:100%;border-radius:4px;background:var(--accent);transition:width 0.3s" :style="'width:' + costBarWidth(m)"></div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div x-show="!costByModelSorted().length" class="text-sm text-dim" style="padding:20px;text-align:center">No model cost data yet.</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Page: Sessions -->
<template x-if="page === 'sessions'">
<div x-data="sessionsPage">
<div class="page-header">
<h2>Sessions</h2>
<input class="form-input" style="width:200px" placeholder="Filter by agent..." x-model="searchFilter" x-show="tab === 'sessions'">
</div>
<div class="tabs">
<div class="tab" :class="{ active: tab === 'sessions' }" @click="tab = 'sessions'">Sessions</div>
<div class="tab" :class="{ active: tab === 'memory' }" @click="tab = 'memory'">Memory</div>
</div>
<div class="page-body" x-init="loadSessions()">
<!-- Sessions tab -->
<div x-show="tab === 'sessions'">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading sessions...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<div class="card mb-4" style="border-left:3px solid var(--accent)">
<div class="font-bold" style="font-size:13px;margin-bottom:4px">Conversation Sessions</div>
<div class="text-sm text-dim" style="line-height:1.6">
Each conversation with an agent creates a session. Sessions store the full message history so you can resume conversations later, or review past interactions.
</div>
</div>
<div class="table-wrap" x-show="filteredSessions.length">
<table>
<thead><tr><th>Session</th><th>Agent</th><th>Messages</th><th>Created</th><th>Actions</th></tr></thead>
<tbody>
<template x-for="s in filteredSessions" :key="s.session_id">
<tr>
<td class="text-xs truncate" style="font-family:monospace;max-width:120px" x-text="s.session_id ? s.session_id.substring(0, 8) + '...' : '-'" :title="s.session_id"></td>
<td class="font-bold" x-text="s.agent_name || s.agent_id"></td>
<td x-text="s.message_count"></td>
<td class="text-xs" x-text="s.created_at ? new Date(s.created_at).toLocaleString() : '-'"></td>
<td>
<button class="btn btn-primary btn-sm" @click="openInChat(s)">Chat</button>
<button class="btn btn-danger btn-sm" @click="deleteSession(s.session_id)">Delete</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!filteredSessions.length && !searchFilter">
<div class="empty-state-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 2-10 5 10 5 10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/></svg>
</div>
<h3>No sessions yet</h3>
<p>Sessions are created when you chat with agents. Start a conversation to see session history here.</p>
<button class="btn btn-primary" @click="location.hash='agents'">Start Chatting</button>
</div>
<div class="empty-state" x-show="!filteredSessions.length && searchFilter"><p>No sessions match your filter.</p></div>
</div>
</div>
<!-- Memory tab -->
<div x-show="tab === 'memory'">
<div class="flex justify-between items-center mb-4">
<div class="info-card" style="flex:1;margin-bottom:0">
<h4>Agent Memory</h4>
<p>Each agent has its own key-value memory store. Agents use memory to persist preferences, notes, and context between conversations.</p>
</div>
<select class="form-select" style="width:200px;margin-left:16px" x-model="memAgentId" @change="loadKv()">
<option value="">Select agent...</option>
<template x-for="a in $store.app.agents" :key="a.id">
<option :value="a.id" x-text="a.name"></option>
</template>
</select>
</div>
<div x-show="memLoading" class="loading-state"><div class="spinner"></div><span>Loading memory...</span></div>
<div x-show="!memLoading && memLoadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="memLoadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadKv()">Retry</button>
</div>
<div x-show="memAgentId && !memLoading && !memLoadError">
<div class="flex justify-between items-center mb-4">
<span class="text-sm text-dim" x-text="kvPairs.length + ' key(s)'"></span>
<button class="btn btn-primary btn-sm" @click="showAdd = true">+ Add Key</button>
</div>
<div class="table-wrap" x-show="kvPairs.length">
<table>
<thead><tr><th>Key</th><th>Value</th><th style="width:140px">Actions</th></tr></thead>
<tbody>
<template x-for="kv in kvPairs" :key="kv.key">
<tr>
<td class="font-bold" style="white-space:nowrap" x-text="kv.key"></td>
<td>
<template x-if="editingKey !== kv.key">
<pre style="font-size:11px;max-width:400px;overflow:auto;white-space:pre-wrap;margin:0;color:var(--text-dim)" x-text="typeof kv.value === 'object' ? JSON.stringify(kv.value, null, 2) : String(kv.value)"></pre>
</template>
<template x-if="editingKey === kv.key">
<div>
<textarea class="form-textarea" x-model="editingValue" style="font-size:11px;min-height:60px;font-family:var(--font-mono)"></textarea>
<div class="flex gap-2 mt-2">
<button class="btn btn-primary btn-sm" @click="saveEdit()">Save</button>
<button class="btn btn-ghost btn-sm" @click="cancelEdit()">Cancel</button>
</div>
</div>
</template>
</td>
<td>
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm" @click="startEdit(kv)" x-show="editingKey !== kv.key">Edit</button>
<button class="btn btn-danger btn-sm" @click="deleteKey(kv.key)" x-show="editingKey !== kv.key">Delete</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!kvPairs.length">
<h4>No keys stored</h4>
<p class="hint">This agent has no memory entries yet. Agents create memory entries automatically during conversations, or you can add them manually.</p>
<button class="btn btn-primary mt-4" @click="showAdd = true">+ Add First Key</button>
</div>
<template x-if="showAdd">
<div class="modal-overlay" @click.self="showAdd = false" @keydown.escape.window="showAdd = false">
<div class="modal">
<div class="modal-header"><h3>Add Key</h3><button class="modal-close" @click="showAdd = false">&times;</button></div>
<div class="form-group"><label>Key</label><input class="form-input" x-model="newKey" placeholder="my_key"></div>
<div class="form-group"><label>Value (JSON)</label><textarea class="form-textarea" x-model="newValue" placeholder='"hello"'></textarea></div>
<button class="btn btn-primary btn-block" @click="addKey()">Save</button>
</div>
</div>
</template>
</div>
<div class="empty-state" x-show="!memAgentId && !memLoading">
<h4>Select an Agent</h4>
<p class="hint">Choose an agent from the dropdown above to browse and edit its memory store.</p>
</div>
</div>
</div>
</div>
</template>
<!-- Page: Logs -->
<template x-if="page === 'logs'">
<div x-data="logsPage">
<div class="page-header">
<h2>Logs</h2>
<div class="flex gap-2 items-center" x-show="tab === 'live'">
<!-- Connection status indicator -->
<span class="live-indicator" :class="connectionClass">
<span class="live-dot"></span>
<span x-text="connectionLabel"></span>
</span>
<select class="form-select" style="width:100px" x-model="levelFilter">
<option value="">All</option>
<option value="info">INFO</option>
<option value="warn">WARN</option>
<option value="error">ERROR</option>
</select>
<input class="form-input" style="width:180px" placeholder="Search..." x-model="textFilter">
<button class="btn btn-ghost btn-sm" @click="togglePause()" x-text="streamPaused ? 'Resume' : 'Pause'"></button>
<button class="btn btn-ghost btn-sm" @click="clearLogs()">Clear</button>
<button class="btn btn-ghost btn-sm" @click="exportLogs()">Export</button>
<div class="toggle" :class="{ active: autoRefresh }" @click="autoRefresh = !autoRefresh" title="Auto-scroll"></div>
<span class="text-xs text-dim" x-text="autoRefresh ? 'Auto-scroll' : 'Scroll locked'"></span>
</div>
<div class="flex gap-2 items-center" x-show="tab === 'audit'">
<button class="btn btn-ghost btn-sm" @click="verifyChain()">Verify Chain</button>
<span class="badge" :class="chainValid === true ? 'badge-running' : chainValid === false ? 'badge-crashed' : ''" x-text="chainValid === true ? 'VALID' : chainValid === false ? 'BROKEN' : ''" x-show="chainValid !== null"></span>
</div>
</div>
<div class="tabs">
<div class="tab" :class="{ active: tab === 'live' }" @click="tab = 'live'">Live</div>
<div class="tab" :class="{ active: tab === 'audit' }" @click="tab = 'audit'; if (!auditEntries.length && !auditLoading) loadAudit()">Audit Trail</div>
</div>
<div class="page-body" x-init="startStreaming()">
<!-- Live logs tab -->
<div x-show="tab === 'live'">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Connecting to log stream...</span></div>
<div x-show="!loading && loadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="loadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !loadError">
<div class="card" style="font-family:monospace;max-height:70vh;overflow-y:auto" id="log-container" @mouseenter="hovering = true" @mouseleave="hovering = false">
<template x-for="entry in filteredEntries" :key="entry.seq">
<div class="log-entry">
<span class="log-timestamp" x-text="new Date(entry.timestamp).toLocaleTimeString()"></span>
<span class="log-level" :class="'log-level-' + classifyLevel(entry.action)" x-text="classifyLevel(entry.action).toUpperCase()"></span>
<span class="text-xs" style="color:var(--text-dim);margin-right:6px" x-text="'[' + entry.action + ']'"></span>
<span class="text-xs" x-text="entry.detail"></span>
</div>
</template>
<div class="empty-state" x-show="!filteredEntries.length" style="padding:20px">
<h4>No log entries yet</h4>
<p class="hint">Activity will appear here as agents run.</p>
</div>
</div>
</div>
</div>
<!-- Audit trail tab -->
<div x-show="tab === 'audit'">
<div x-show="auditLoading" class="loading-state"><div class="spinner"></div><span>Loading audit log...</span></div>
<div x-show="!auditLoading && auditLoadError" class="error-state">
<span class="error-icon">!</span>
<p x-text="auditLoadError"></p>
<button class="btn btn-ghost btn-sm" @click="loadAudit()">Retry</button>
</div>
<div x-show="!auditLoading && !auditLoadError">
<div class="card mb-4" style="border-left:3px solid var(--accent)">
<div class="font-bold" style="font-size:13px;margin-bottom:4px">Tamper-Evident Audit Trail</div>
<div class="text-sm text-dim" style="line-height:1.6">
Every agent action is logged with a cryptographic hash chain. Use "Verify Chain" to confirm no entries have been altered or deleted.
</div>
</div>
<div class="flex gap-2 mb-4 items-center">
<select class="form-select" style="width:180px" x-model="filterAction">
<option value="">All Actions</option>
<option value="AgentSpawn">Agent Created</option>
<option value="AgentKill">Agent Stopped</option>
<option value="AgentMessage">Message</option>
<option value="ToolInvoke">Tool Used</option>
<option value="NetworkAccess">Network Access</option>
<option value="ShellExec">Shell Command</option>
<option value="FileAccess">File Access</option>
<option value="MemoryAccess">Memory Access</option>
<option value="AuthAttempt">Login Attempt</option>
</select>
<span class="text-sm text-dim" x-text="filteredAuditEntries.length + ' of ' + auditEntries.length + ' entries'"></span>
<span class="text-xs text-dim" x-show="tipHash" x-text="'tip: ' + tipHash.substring(0, 16) + '...'"></span>
</div>
<div class="table-wrap" x-show="filteredAuditEntries.length">
<table>
<thead><tr><th>#</th><th>Timestamp</th><th>Agent</th><th>Action</th><th>Detail</th><th>Outcome</th></tr></thead>
<tbody>
<template x-for="e in filteredAuditEntries" :key="e.seq">
<tr>
<td x-text="e.seq"></td>
<td class="text-xs" style="white-space:nowrap" x-text="new Date(e.timestamp).toLocaleString()"></td>
<td class="truncate" style="max-width:120px" x-text="auditAgentName(e.agent_id)" :title="e.agent_id"></td>
<td><span class="badge badge-created" x-text="friendlyAction(e.action)"></span></td>
<td class="truncate" style="max-width:200px" x-text="e.detail" :title="e.detail"></td>
<td x-text="e.outcome"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="empty-state" x-show="!auditEntries.length">
<h4>No audit entries yet</h4>
<p class="hint">Activity will appear here as agents operate.</p>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Page: Setup Wizard -->
<template x-if="page === 'wizard'">
<div x-data="wizardPage">
<div class="page-header">
<h2>Setup Wizard</h2>
<button class="btn btn-ghost btn-sm" @click="finishAndDismiss()">Skip Setup</button>
</div>
<div class="page-body" x-init="loadData()">
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading...</span></div>
<div x-show="!loading && error" class="error-state">
<span class="error-icon">!</span>
<p x-text="error"></p>
<button class="btn btn-ghost btn-sm" @click="loadData()">Retry</button>
</div>
<div x-show="!loading && !error">
<!-- Progress bar -->
<div class="wizard-progress">
<template x-for="n in totalSteps" :key="n">
<div class="wizard-progress-step" :class="{ 'wiz-active': step === n, 'wiz-done': step > n }" @click="goToStep(n)">
<div class="wizard-progress-circle">
<span x-show="step <= n" x-text="n"></span>
<span x-show="step > n">&#10003;</span>
</div>
<span class="wizard-progress-label" x-text="stepLabel(n)"></span>
</div>
</template>
<div class="wizard-progress-line">
<div class="wizard-progress-line-fill" :style="'width:' + ((step - 1) / (totalSteps - 1) * 100) + '%'"></div>
</div>
</div>
<!-- Step 1: Welcome -->
<div class="wizard-step" x-show="step === 1">
<div class="wizard-card" style="text-align:center;max-width:600px;margin:0 auto">
<img src="/logo.png" alt="OpenFang" style="width:80px;height:80px;margin:0 auto 20px;display:block;opacity:0.85">
<h3 style="font-size:22px;font-weight:700;margin-bottom:12px;color:var(--accent)">Welcome to OpenFang</h3>
<p style="font-size:13px;color:var(--text-dim);line-height:1.8;max-width:480px;margin:0 auto 24px">
OpenFang is an open-source Agent Operating System. It lets you run AI agents that can chat, use tools, access memory, and connect to messaging channels &mdash; all from a single dashboard.
</p>
<div class="card" style="text-align:left;margin-bottom:20px">
<div class="card-header">This wizard will help you:</div>
<div style="margin-top:8px">
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border)">
<span class="badge badge-info" style="min-width:20px;justify-content:center">1</span>
<span style="font-size:12px">Connect an LLM provider (Anthropic, OpenAI, Gemini, etc.)</span>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border)">
<span class="badge badge-info" style="min-width:20px;justify-content:center">2</span>
<span style="font-size:12px">Create your first AI agent from 10 templates</span>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border)">
<span class="badge badge-info" style="min-width:20px;justify-content:center">3</span>
<span style="font-size:12px">Try it out with a quick test message</span>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:8px 0">
<span class="badge badge-info" style="min-width:20px;justify-content:center">4</span>
<span style="font-size:12px">Optionally connect a messaging channel (Telegram, Discord, Slack)</span>
</div>
</div>
</div>
<p style="font-size:11px;color:var(--text-muted)">Takes about 2 minutes. You can skip any step and configure later.</p>
</div>
<div class="wizard-nav">
<div></div>
<button class="btn btn-primary" @click="nextStep()">Get Started</button>
</div>
</div>
<!-- Step 2: Provider Setup -->
<div class="wizard-step" x-show="step === 2">
<div class="wizard-card">
<h3 style="font-size:16px;font-weight:700;margin-bottom:4px">Connect an LLM Provider</h3>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px;line-height:1.6">
OpenFang needs at least one LLM provider to power your agents. Select a provider and enter your API key.
</p>
<div class="info-card" x-show="hasConfiguredProvider" style="border-left-color:var(--success)">
<h4 style="color:var(--success)">Provider Already Configured</h4>
<p>You already have at least one provider set up. You can continue to the next step or configure additional providers.</p>
</div>
<div style="margin-bottom:16px">
<div class="text-xs font-bold text-dim mb-2" style="text-transform:uppercase;letter-spacing:0.5px">Popular Providers</div>
<div class="card-grid" style="grid-template-columns:repeat(auto-fill, minmax(200px, 1fr))">
<template x-for="p in popularProviders" :key="p.id">
<div class="card wizard-provider-card" :class="{ 'wizard-provider-selected': selectedProvider === p.id, 'provider-card configured': providerIsConfigured(p) }" @click="selectProvider(p.id)" style="cursor:pointer;padding:12px">
<div class="flex justify-between items-center">
<span class="font-bold" style="font-size:13px" x-text="p.display_name"></span>
<span class="badge badge-success" x-show="providerIsConfigured(p)" style="font-size:8px">READY</span>
</div>
<div class="text-xs text-dim mt-1" x-text="(p.model_count || 0) + ' models'"></div>
</div>
</template>
</div>
</div>
<div style="margin-bottom:16px" x-show="otherProviders.length">
<div class="text-xs font-bold text-dim mb-2" style="text-transform:uppercase;letter-spacing:0.5px">Other Providers</div>
<div class="card-grid" style="grid-template-columns:repeat(auto-fill, minmax(200px, 1fr))">
<template x-for="p in otherProviders" :key="p.id">
<div class="card wizard-provider-card" :class="{ 'wizard-provider-selected': selectedProvider === p.id, 'provider-card configured': providerIsConfigured(p) }" @click="selectProvider(p.id)" style="cursor:pointer;padding:12px">
<div class="flex justify-between items-center">
<span class="font-bold" style="font-size:13px" x-text="p.display_name"></span>
<span class="badge badge-success" x-show="providerIsConfigured(p)" style="font-size:8px">READY</span>
</div>
<div class="text-xs text-dim mt-1" x-text="(p.model_count || 0) + ' models'"></div>
</div>
</template>
</div>
</div>
<template x-if="selectedProviderObj && !providerIsConfigured(selectedProviderObj)">
<div class="card" style="border-left:3px solid var(--accent);margin-top:16px">
<div class="card-header" x-text="'Configure ' + selectedProviderObj.display_name"></div>
<div class="text-xs text-dim mb-2" x-show="selectedProviderObj.api_key_env">
Environment variable: <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px" x-text="selectedProviderObj.api_key_env"></code>
</div>
<div class="text-xs mb-3" x-show="providerHelp(selectedProvider)" style="color:var(--accent-light)">
<a :href="providerHelp(selectedProvider) ? providerHelp(selectedProvider).url : '#'" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:underline" x-text="providerHelp(selectedProvider) ? providerHelp(selectedProvider).text : ''"></a>
</div>
<div class="form-group">
<label>API Key</label>
<div class="key-input-group">
<input type="password" :placeholder="'Enter your ' + selectedProviderObj.display_name + ' API key'" x-model="apiKeyInput" @keydown.enter="saveKey()">
<button class="btn btn-primary btn-sm" @click="saveKey()" :disabled="savingKey || !apiKeyInput.trim()">
<span x-show="!savingKey">Save &amp; Test</span>
<span x-show="savingKey" class="spinner" style="width:10px;height:10px;border-width:2px"></span>
</button>
</div>
</div>
<div x-show="testResult" class="mt-2">
<div x-show="testResult && testResult.status === 'ok'" class="badge badge-success" style="padding:6px 12px">Connected successfully<span x-show="testResult && testResult.latency_ms" x-text="' (' + (testResult ? testResult.latency_ms : '') + 'ms)'"></span></div>
<div x-show="testResult && testResult.status !== 'ok'" class="badge badge-error" style="padding:6px 12px"><span x-text="testResult ? (testResult.error || 'Connection failed') : ''"></span></div>
</div>
</div>
</template>
<template x-if="selectedProviderObj && providerIsConfigured(selectedProviderObj)">
<div class="card" style="border-left:3px solid var(--success);margin-top:16px">
<div class="flex items-center gap-2">
<span style="color:var(--success);font-size:18px">&#10003;</span>
<div>
<div class="font-bold" style="font-size:13px" x-text="selectedProviderObj.display_name + ' is configured and ready'"></div>
<div class="text-xs text-dim">You can test the connection or continue to the next step.</div>
</div>
</div>
<div class="flex gap-2 mt-2">
<button class="btn btn-ghost btn-sm" @click="testKey()" :disabled="testingProvider">
<span x-show="!testingProvider">Test Connection</span>
<span x-show="testingProvider" class="spinner" style="width:10px;height:10px;border-width:2px"></span>
</button>
</div>
<div x-show="testResult" class="mt-2">
<div x-show="testResult && testResult.status === 'ok'" class="badge badge-success" style="padding:6px 12px">Connected<span x-show="testResult && testResult.latency_ms" x-text="' (' + (testResult ? testResult.latency_ms : '') + 'ms)'"></span></div>
<div x-show="testResult && testResult.status !== 'ok'" class="badge badge-error" style="padding:6px 12px"><span x-text="testResult ? (testResult.error || 'Connection failed') : ''"></span></div>
</div>
</div>
</template>
</div>
<div class="wizard-nav">
<button class="btn btn-ghost" @click="prevStep()">Back</button>
<button class="btn btn-primary" @click="nextStep()" :disabled="!canGoNext"><span x-text="hasConfiguredProvider || keySaved ? 'Next' : 'Skip'"></span></button>
</div>
</div>
<!-- Step 3: Create First Agent -->
<div class="wizard-step" x-show="step === 3">
<div class="wizard-card">
<h3 style="font-size:16px;font-weight:700;margin-bottom:4px">Create Your First Agent</h3>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px;line-height:1.6">Pick a template to get started quickly. You can customize the agent later or create more from the Agents page.</p>
<!-- Category filter pills -->
<div class="wizard-category-pills">
<template x-for="cat in templateCategories" :key="cat">
<button class="wizard-category-pill" :class="{ active: templateCategory === cat }" @click="templateCategory = cat" x-text="cat"></button>
</template>
</div>
<div class="card-grid" style="grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));margin-bottom:20px">
<template x-for="(tpl, i) in filteredTemplates" :key="tpl.id">
<div class="card wizard-template-card" :class="{ 'wizard-template-selected': selectedTemplate === templates.indexOf(tpl) }" @click="selectTemplate(templates.indexOf(tpl))" style="cursor:pointer">
<div class="flex items-center gap-2 mb-2">
<span class="channel-icon" style="background:var(--accent);color:var(--bg-primary);font-weight:700" x-text="tpl.icon"></span>
<div>
<span class="font-bold" style="font-size:13px" x-text="tpl.name"></span>
<span class="category-badge" style="margin-left:6px" x-text="tpl.category"></span>
</div>
</div>
<div class="text-xs text-dim" style="line-height:1.6" x-text="tpl.description"></div>
<div class="flex justify-between items-center mt-2">
<span class="text-xs" style="color:var(--text-muted)" x-text="tpl.provider + ' / ' + tpl.model"></span>
<span class="badge badge-muted" x-text="tpl.profile"></span>
</div>
<div class="text-xs mt-1 text-dim" x-show="profileInfo(tpl.profile).desc" x-text="profileInfo(tpl.profile).desc"></div>
</div>
</template>
</div>
<div class="card" style="border-left:3px solid var(--accent)">
<div class="form-group" style="margin-bottom:8px">
<label>Agent Name</label>
<input class="form-input" type="text" x-model="agentName" placeholder="my-assistant" style="max-width:320px" @keydown.enter="createAgent()">
</div>
<div class="text-xs text-dim" x-text="'Will use ' + templates[selectedTemplate].provider + ' / ' + templates[selectedTemplate].model + ' with ' + profileInfo(templates[selectedTemplate].profile).label + ' profile'"></div>
<div class="mt-2">
<button class="btn btn-primary" @click="createAgent()" :disabled="creatingAgent || !agentName.trim()">
<span x-show="!creatingAgent">Create Agent</span>
<span x-show="creatingAgent" class="spinner" style="width:10px;height:10px;border-width:2px"></span>
</button>
</div>
<div x-show="createdAgent" class="mt-2">
<div class="badge badge-success" style="padding:6px 12px">Agent "<span x-text="createdAgent ? createdAgent.name : ''"></span>" created successfully</div>
</div>
</div>
</div>
<div class="wizard-nav">
<button class="btn btn-ghost" @click="prevStep()">Back</button>
<button class="btn btn-primary" @click="nextStep()"><span x-text="createdAgent ? 'Next: Try It' : 'Skip'"></span></button>
</div>
</div>
<!-- Step 4: Try It (mini chat) -->
<div class="wizard-step" x-show="step === 4">
<div class="wizard-card">
<h3 style="font-size:16px;font-weight:700;margin-bottom:4px">Try Your Agent</h3>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px;line-height:1.6">
Send a quick message to test your new agent. Try one of the suggestions below or type your own.
</p>
<!-- Suggested message chips -->
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px">
<template x-for="(s, si) in currentSuggestions" :key="si">
<button class="suggest-chip" @click="sendTryItMessage(s)" :disabled="tryItSending" x-text="s"></button>
</template>
</div>
<!-- Mini chat messages -->
<div class="tryit-messages" style="min-height:60px">
<template x-for="(msg, mi) in tryItMessages" :key="mi">
<div class="tryit-msg" :class="msg.role === 'user' ? 'tryit-msg-user' : 'tryit-msg-agent'" x-text="msg.text"></div>
</template>
<div x-show="tryItSending" class="tryit-msg tryit-msg-agent" style="opacity:0.5">Thinking...</div>
</div>
<!-- Input -->
<div style="display:flex;gap:8px;margin-top:12px">
<input class="form-input" type="text" x-model="tryItInput" placeholder="Type a message..."
@keydown.enter="sendTryItMessage(tryItInput)" :disabled="tryItSending" style="flex:1">
<button class="btn btn-primary btn-sm" @click="sendTryItMessage(tryItInput)" :disabled="tryItSending || !tryItInput.trim()">Send</button>
</div>
</div>
<div class="wizard-nav">
<button class="btn btn-ghost" @click="prevStep()">Back</button>
<button class="btn btn-primary" @click="nextStep()">Continue</button>
</div>
</div>
<!-- Step 5: Channel Setup (Optional) -->
<div class="wizard-step" x-show="step === 5">
<div class="wizard-card">
<h3 style="font-size:16px;font-weight:700;margin-bottom:4px">Connect a Channel <span class="badge badge-muted">Optional</span></h3>
<p style="font-size:12px;color:var(--text-dim);margin-bottom:16px;line-height:1.6">Channels let your agent communicate via messaging platforms. This is optional &mdash; you can always use the built-in web chat.</p>
<div class="card-grid" style="grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));margin-bottom:16px">
<template x-for="ch in channelOptions" :key="ch.name">
<div class="card wizard-template-card" :class="{ 'wizard-template-selected': channelType === ch.name }" @click="selectChannel(ch.name)" style="cursor:pointer">
<div class="flex items-center gap-2 mb-2">
<span class="channel-icon" x-text="ch.icon"></span>
<span class="font-bold" style="font-size:13px" x-text="ch.display_name"></span>
</div>
<div class="text-xs text-dim" style="line-height:1.6" x-text="ch.description"></div>
</div>
</template>
</div>
<template x-if="selectedChannelObj">
<div class="card" style="border-left:3px solid var(--accent)">
<div class="card-header" x-text="'Configure ' + selectedChannelObj.display_name"></div>
<div class="text-xs text-dim mb-2" x-text="selectedChannelObj.help"></div>
<div class="form-group">
<label x-text="selectedChannelObj.token_label"></label>
<div class="key-input-group">
<input type="password" :placeholder="selectedChannelObj.token_placeholder" x-model="channelToken" @keydown.enter="configureChannel()">
<button class="btn btn-primary btn-sm" @click="configureChannel()" :disabled="configuringChannel || !channelToken.trim()">
<span x-show="!configuringChannel">Save</span>
<span x-show="configuringChannel" class="spinner" style="width:10px;height:10px;border-width:2px"></span>
</button>
</div>
</div>
<div class="text-xs text-dim" x-text="'Or set ' + selectedChannelObj.token_env + ' in your environment'"></div>
<div x-show="channelConfigured" class="mt-2">
<div class="badge badge-success" style="padding:6px 12px"><span x-text="selectedChannelObj ? selectedChannelObj.display_name : ''"></span> configured and activated.</div>
</div>
</div>
</template>
<div class="info-card" x-show="!channelType">
<p>You can skip this step. The built-in web chat is always available from the <strong>Agents</strong> page. Add channels any time from <strong>Settings &rarr; Channels</strong>.</p>
</div>
</div>
<div class="wizard-nav">
<button class="btn btn-ghost" @click="prevStep()">Back</button>
<button class="btn btn-primary" @click="nextStep()"><span x-text="channelConfigured ? 'Next' : 'Skip'"></span></button>
</div>
</div>
<!-- Step 6: Done -->
<div class="wizard-step" x-show="step === 6">
<div class="wizard-card" style="text-align:center;max-width:560px;margin:0 auto">
<div style="font-size:56px;margin-bottom:12px;color:var(--success)">&#10003;</div>
<h3 style="font-size:20px;font-weight:700;margin-bottom:8px;color:var(--accent)">You're All Set!</h3>
<p style="font-size:13px;color:var(--text-dim);line-height:1.8;margin-bottom:24px">OpenFang is configured and ready to go. Here is a summary of what was set up:</p>
<div class="card" style="text-align:left;margin-bottom:20px">
<div class="detail-grid">
<div class="detail-row">
<span class="detail-label">LLM Provider</span>
<span class="detail-value">
<span x-show="setupSummary.provider" x-text="setupSummary.provider"></span>
<span x-show="!setupSummary.provider && hasConfiguredProvider" class="badge badge-success">Pre-configured</span>
<span x-show="!setupSummary.provider && !hasConfiguredProvider" class="badge badge-warn">Skipped</span>
</span>
</div>
<div class="detail-row">
<span class="detail-label">First Agent</span>
<span class="detail-value">
<span x-show="setupSummary.agent" x-text="setupSummary.agent"></span>
<span x-show="!setupSummary.agent" class="badge badge-warn">Skipped</span>
</span>
</div>
<div class="detail-row">
<span class="detail-label">Channel</span>
<span class="detail-value">
<span x-show="setupSummary.channel" x-text="setupSummary.channel"></span>
<span x-show="!setupSummary.channel" class="badge badge-muted">None (web chat available)</span>
</span>
</div>
</div>
</div>
<div class="card" style="text-align:left;margin-bottom:20px">
<div class="card-header">Next Steps</div>
<div style="margin-top:8px;font-size:12px;color:var(--text-dim);line-height:1.8">
<div style="padding:4px 0" x-show="createdAgent">&#8226; Open <strong>Agents</strong> to start talking to your agent</div>
<div style="padding:4px 0" x-show="!createdAgent">&#8226; Go to <strong>Agents</strong> to create your first agent</div>
<div style="padding:4px 0">&#8226; Browse <strong>Skills</strong> to add capabilities (web search, code execution, etc.)</div>
<div style="padding:4px 0">&#8226; Check <strong>Settings</strong> for advanced configuration</div>
<div style="padding:4px 0" x-show="!setupSummary.channel">&#8226; Visit <strong>Channels</strong> to connect messaging platforms</div>
</div>
</div>
<div class="flex gap-2" style="justify-content:center">
<button class="btn btn-primary" @click="finish()" x-text="createdAgent ? 'Start Chatting' : 'Go to Dashboard'"></button>
<button class="btn btn-ghost" @click="prevStep()">Back</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</main>
</div>
<!-- Toast notification container -->
<div id="toast-container" class="toast-container" aria-live="polite"></div>