fix: comprehensive iPhone UI overhaul — glassmorphism, responsive layouts, theme unification
- base.html: add missing {% block extra_styles %}, mobile hamburger menu with
slide-out nav, interactive-widget viewport meta, -webkit-text-size-adjust
- style.css: define 15+ missing CSS variables (--bg-secondary, --text-muted,
--accent, --success, --danger, etc.), add missing utility classes (.grid,
.stat, .agent-card, .agent-avatar, .form-group), glassmorphism card effects,
iPhone breakpoints (768px, 390px), 44pt min touch targets, smooth animations
- mobile.html: rewrite with proper theme variables, glass cards, touch-friendly
quick actions grid, chat with proper message bubbles
- swarm_live.html: replace undefined CSS vars, use mc-panel theme cards
- marketplace.html: responsive agent cards that stack on iPhone, themed pricing
- voice_button.html & voice_enhanced.html: proper theme integration, touch-sized
buttons, themed result containers
- create_task.html: mobile-friendly forms with 16px font (prevents iOS zoom)
- tools.html & creative.html: themed headers, responsive column stacking
- spark.html: replace all hardcoded blue (#00d4ff) colors with theme purple/orange
- briefing.html: replace hardcoded bootstrap colors with theme variables
Fixes: header nav overflow on iPhone (7 links in single row), missing
extra_styles block silently dropping child template styles, undefined CSS
variables breaking mobile/swarm/marketplace/voice pages, sub-44pt touch
targets, missing -webkit-text-size-adjust, inconsistent color themes.
97 UI tests pass (91 UI-specific + 6 creative route).
https://claude.ai/code/session_01JiyhGyee2zoMN4p8xWYqEe
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="theme-color" content="#080412" />
|
||||
@@ -11,40 +11,91 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="/static/style.css?v=2" />
|
||||
<link rel="stylesheet" href="/static/style.css?v=3" />
|
||||
{% block extra_styles %}{% endblock %}
|
||||
<script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="mc-header">
|
||||
<div class="mc-header-left">
|
||||
<span class="mc-title">TIMMY TIME</span>
|
||||
<a href="/" class="mc-title">TIMMY TIME</a>
|
||||
<span class="mc-subtitle">MISSION CONTROL</span>
|
||||
</div>
|
||||
<div class="mc-header-right">
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<div class="mc-header-right mc-desktop-nav">
|
||||
<a href="/briefing" class="mc-test-link">BRIEFING</a>
|
||||
<a href="/swarm/live" class="mc-test-link">SWARM</a>
|
||||
<a href="/spark/ui" class="mc-test-link">SPARK</a>
|
||||
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
|
||||
<a href="/tools" class="mc-test-link">TOOLS</a>
|
||||
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
|
||||
<a href="/mobile" class="mc-test-link">MOBILE</a>
|
||||
<button id="enable-notifications" class="mc-test-link" style="background:none;border:none;cursor:pointer;" title="Enable notifications">🔔</button>
|
||||
<button id="enable-notifications" class="mc-test-link" style="background:none;cursor:pointer;" title="Enable notifications">🔔</button>
|
||||
<span class="mc-time" id="clock"></span>
|
||||
</div>
|
||||
|
||||
<!-- Mobile hamburger -->
|
||||
<button class="mc-hamburger" id="hamburger-btn" aria-label="Menu">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Mobile slide-out menu -->
|
||||
<div class="mc-mobile-overlay" id="mobile-overlay"></div>
|
||||
<nav class="mc-mobile-menu" id="mobile-menu">
|
||||
<div class="mc-mobile-menu-header">
|
||||
<span class="mc-mobile-menu-title">NAVIGATE</span>
|
||||
<span class="mc-time" id="clock-mobile"></span>
|
||||
</div>
|
||||
<a href="/" class="mc-mobile-link">HOME</a>
|
||||
<a href="/briefing" class="mc-mobile-link">BRIEFING</a>
|
||||
<a href="/swarm/live" class="mc-mobile-link">SWARM</a>
|
||||
<a href="/spark/ui" class="mc-mobile-link">SPARK</a>
|
||||
<a href="/marketplace/ui" class="mc-mobile-link">MARKET</a>
|
||||
<a href="/tools" class="mc-mobile-link">TOOLS</a>
|
||||
<a href="/creative/ui" class="mc-mobile-link">CREATIVE</a>
|
||||
<a href="/voice/button" class="mc-mobile-link">VOICE</a>
|
||||
<a href="/mobile" class="mc-mobile-link">MOBILE</a>
|
||||
<div class="mc-mobile-menu-footer">
|
||||
<button id="enable-notifications-mobile" class="mc-mobile-link" style="background:none;border:none;cursor:pointer;width:100%;text-align:left;font:inherit;color:inherit;padding:inherit;">🔔 NOTIFICATIONS</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="mc-main">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Clock
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
document.getElementById('clock').textContent =
|
||||
now.toLocaleTimeString('en-US', { hour12: false });
|
||||
const t = new Date().toLocaleTimeString('en-US', { hour12: false });
|
||||
const el = document.getElementById('clock');
|
||||
const el2 = document.getElementById('clock-mobile');
|
||||
if (el) el.textContent = t;
|
||||
if (el2) el2.textContent = t;
|
||||
}
|
||||
setInterval(updateClock, 1000);
|
||||
updateClock();
|
||||
|
||||
// Mobile menu
|
||||
const hamburger = document.getElementById('hamburger-btn');
|
||||
const overlay = document.getElementById('mobile-overlay');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
function toggleMenu() {
|
||||
const open = menu.classList.toggle('open');
|
||||
overlay.classList.toggle('open', open);
|
||||
hamburger.classList.toggle('open', open);
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
}
|
||||
hamburger.addEventListener('click', toggleMenu);
|
||||
overlay.addEventListener('click', toggleMenu);
|
||||
|
||||
// Highlight current page in mobile menu
|
||||
document.querySelectorAll('.mc-mobile-link').forEach(function(a) {
|
||||
if (a.getAttribute('href') === window.location.pathname) {
|
||||
a.classList.add('active');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmE6bgx5xwkVYG3WhIEOFSjBqg4X" crossorigin="anonymous"></script>
|
||||
<script src="/static/notifications.js"></script>
|
||||
|
||||
@@ -2,10 +2,154 @@
|
||||
|
||||
{% block title %}Timmy Time — Morning Briefing{% endblock %}
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
/* ── Briefing-specific styles ── */
|
||||
|
||||
.briefing-container { max-width: 680px; }
|
||||
|
||||
.briefing-header {
|
||||
border-left: 3px solid var(--amber);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.briefing-greeting {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--amber);
|
||||
letter-spacing: 0.04em;
|
||||
font-family: var(--font);
|
||||
}
|
||||
.briefing-timestamp {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.briefing-ts-val { color: var(--text); }
|
||||
|
||||
.briefing-prose {
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
color: var(--text-bright);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Approval cards */
|
||||
.approval-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: rgba(24, 10, 45, 0.5);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.approval-card.approved {
|
||||
border-color: var(--green);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.approval-card.rejected {
|
||||
border-color: var(--red);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.approval-card-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-bright);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.approval-card-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.approval-card-action {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--font);
|
||||
margin-bottom: 0.75rem;
|
||||
border-left: 2px solid var(--border);
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.impact-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.impact-low { background: var(--green-dim); color: var(--green); }
|
||||
.impact-medium { background: var(--amber-dim); color: var(--amber); }
|
||||
.impact-high { background: var(--red-dim); color: var(--red); }
|
||||
|
||||
.approval-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn-approve {
|
||||
background: var(--green-dim);
|
||||
color: var(--green);
|
||||
border: 1px solid var(--green);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
min-height: 44px;
|
||||
transition: background 0.15s;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.btn-approve:hover { background: rgba(0, 232, 122, 0.2); }
|
||||
|
||||
.btn-reject {
|
||||
background: transparent;
|
||||
color: var(--red);
|
||||
border: 1px solid var(--red);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
min-height: 44px;
|
||||
transition: background 0.15s;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.btn-reject:hover { background: rgba(255, 68, 85, 0.1); }
|
||||
|
||||
.no-approvals {
|
||||
text-align: center;
|
||||
color: var(--text-dim);
|
||||
padding: 2rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.btn-refresh:hover { border-color: var(--purple); color: var(--text-bright); }
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.briefing-greeting { font-size: 1.3rem; }
|
||||
.briefing-prose { font-size: 0.95rem; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container briefing-container py-4">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="briefing-header mb-4">
|
||||
<div class="briefing-greeting">Good morning.</div>
|
||||
<div class="briefing-timestamp">
|
||||
@@ -18,191 +162,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="card mc-panel briefing-summary mb-5">
|
||||
<div class="card mc-panel briefing-summary mb-4">
|
||||
<div class="card-header mc-panel-header">// TIMMY’S REPORT</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="briefing-prose">{{ briefing.summary | e }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approval Queue -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
|
||||
<span>// APPROVAL QUEUE</span>
|
||||
<span class="badge bg-warning text-dark"
|
||||
id="approval-count">{{ briefing.approval_items | length }} pending</span>
|
||||
<span class="badge badge-warning" id="approval-count">{{ briefing.approval_items | length }} pending</span>
|
||||
</div>
|
||||
<div class="card-body p-3"
|
||||
id="approval-queue"
|
||||
hx-get="/briefing/approvals"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<!-- HTMX fills this on load with live data -->
|
||||
<div class="text-center text-muted py-3">Loading approval items…</div>
|
||||
<div style="text-align:center; color:var(--text-dim); padding:16px; font-size:0.85rem;">Loading approval items…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Briefing-specific styles — mobile-first */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
.briefing-container {
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.briefing-header {
|
||||
border-left: 3px solid var(--mc-amber, #ffc107);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.briefing-greeting {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--mc-amber, #ffc107);
|
||||
letter-spacing: 0.04em;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.briefing-timestamp {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.briefing-ts-val {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.briefing-prose {
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
color: #dee2e6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Approval cards */
|
||||
.approval-card {
|
||||
border: 1px solid #2a3a4a;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: #0d1b2a;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.approval-card.approved {
|
||||
border-color: #198754;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.approval-card.rejected {
|
||||
border-color: #dc3545;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.approval-card-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: #f8f9fa;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.approval-card-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #adb5bd;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.approval-card-action {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin-bottom: 0.75rem;
|
||||
border-left: 2px solid #495057;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.impact-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.impact-low { background: #198754; color: #fff; }
|
||||
.impact-medium { background: #fd7e14; color: #fff; }
|
||||
.impact-high { background: #dc3545; color: #fff; }
|
||||
|
||||
.approval-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-approve {
|
||||
background: #198754;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.btn-approve:hover { background: #157347; }
|
||||
|
||||
.btn-reject {
|
||||
background: transparent;
|
||||
color: #dc3545;
|
||||
border: 1px solid #dc3545;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.btn-reject:hover { background: #dc354520; }
|
||||
|
||||
.no-approvals {
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
padding: 2rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Refresh button */
|
||||
.btn-refresh {
|
||||
background: transparent;
|
||||
color: #adb5bd;
|
||||
border: 1px solid #495057;
|
||||
border-radius: 4px;
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-refresh:hover {
|
||||
color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.briefing-greeting { font-size: 1.3rem; }
|
||||
.briefing-prose { font-size: 0.95rem; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,71 +3,73 @@
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card" style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">➕ Create New Task</h2>
|
||||
<p style="color: var(--text-secondary);">Agents will bid to complete this task</p>
|
||||
</div>
|
||||
|
||||
<form hx-post="/agents/swarm/task/create" hx-target="#task-result" hx-swap="innerHTML">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Task Title *</label>
|
||||
<input type="text" name="title" placeholder="e.g., Research latest Bitcoin developments" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" rows="3" placeholder="Detailed description of what needs to be done..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Required Capabilities (comma-separated)</label>
|
||||
<input type="text" name="requirements" placeholder="web_search, research, writing">
|
||||
<small style="color: var(--text-muted);">Agents with these capabilities will be eligible to bid</small>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Min Bid (sats)</label>
|
||||
<input type="number" name="min_bid" value="1" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Max Bid (sats)</label>
|
||||
<input type="number" name="max_bid" value="1000" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Priority (1-5)</label>
|
||||
<select name="priority">
|
||||
<option value="1">1 - Low</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3" selected>3 - Normal</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5 - Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; margin-top: 20px;">
|
||||
<a href="/agents/timmy/chat" class="btn btn-secondary" style="flex: 1;">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary" style="flex: 2;">
|
||||
Create Task & Start Auction
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="task-result" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
<div style="max-width: 600px; margin: 0 auto;" class="py-3">
|
||||
|
||||
<div class="card" style="max-width: 600px; margin: 20px auto 0;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">How Auctions Work</h2>
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// CREATE NEW TASK</div>
|
||||
<div class="card-body">
|
||||
<p style="color: var(--text-dim); font-size: 0.85rem; margin-bottom: 16px;">Agents will bid to complete this task</p>
|
||||
|
||||
<form hx-post="/agents/swarm/task/create" hx-target="#task-result" hx-swap="innerHTML">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Task Title *</label>
|
||||
<input type="text" name="title" placeholder="e.g., Research latest Bitcoin developments" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" rows="3" placeholder="Detailed description of what needs to be done..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Required Capabilities</label>
|
||||
<input type="text" name="requirements" placeholder="web_search, research, writing">
|
||||
<small>Agents with these capabilities will be eligible to bid</small>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Min Bid (sats)</label>
|
||||
<input type="number" name="min_bid" value="1" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Max Bid (sats)</label>
|
||||
<input type="number" name="max_bid" value="1000" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Priority</label>
|
||||
<select name="priority">
|
||||
<option value="1">1 - Low</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3" selected>3 - Normal</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5 - Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; margin-top: 16px;">
|
||||
<a href="/" class="btn btn-secondary" style="flex: 1; display: flex; align-items: center; justify-content: center; min-height: 44px; text-decoration: none;">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary" style="flex: 2; min-height: 44px;">Create Task & Start Auction</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="task-result" style="margin-top: 16px;"></div>
|
||||
</div>
|
||||
<ol style="color: var(--text-secondary); line-height: 2; padding-left: 20px;">
|
||||
</div>
|
||||
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// HOW AUCTIONS WORK</div>
|
||||
<div class="card-body">
|
||||
<ol style="color: var(--text-dim); line-height: 2.2; padding-left: 18px; font-size: 0.85rem; margin: 0;">
|
||||
<li>You create a task with requirements</li>
|
||||
<li>A 15-second auction begins automatically</li>
|
||||
<li>Eligible agents place bids in satoshis</li>
|
||||
<li>The lowest bid wins the task</li>
|
||||
<li>The winning agent completes the task and earns the sats</li>
|
||||
</ol>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,43 +2,105 @@
|
||||
|
||||
{% block title %}Creative Studio — Mission Control{% endblock %}
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.creative-container { max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
.creative-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.creative-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.creative-subtitle {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.creative-stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.creative-stat-box {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 8px 14px;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
.creative-stat-val {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
.creative-stat-label {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.persona-card .card-header strong { color: var(--text-bright); }
|
||||
.persona-card .card-body { font-size: 0.85rem; }
|
||||
.persona-card .card-body p { color: var(--text-dim); }
|
||||
|
||||
.pipeline-badge {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.creative-title { font-size: 1.1rem; }
|
||||
.creative-header { flex-direction: column; }
|
||||
.creative-stats { width: 100%; }
|
||||
.creative-stat-box { flex: 1; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1 class="display-6">Creative Studio</h1>
|
||||
<p class="text-secondary">Image, music, and video generation — powered by Pixel, Lyra, and Reel</p>
|
||||
<div class="creative-container py-3">
|
||||
|
||||
<div class="creative-header">
|
||||
<div>
|
||||
<div class="creative-title">CREATIVE STUDIO</div>
|
||||
<div class="creative-subtitle">Image, music, and video generation — powered by Pixel, Lyra, and Reel</div>
|
||||
</div>
|
||||
<div class="col-auto d-flex gap-3">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body text-center py-2 px-3">
|
||||
<h4 class="mb-0">{{ image_count }}</h4>
|
||||
<small class="text-secondary">Images</small>
|
||||
</div>
|
||||
<div class="creative-stats">
|
||||
<div class="creative-stat-box">
|
||||
<div class="creative-stat-val">{{ image_count }}</div>
|
||||
<div class="creative-stat-label">IMAGES</div>
|
||||
</div>
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body text-center py-2 px-3">
|
||||
<h4 class="mb-0">{{ music_count }}</h4>
|
||||
<small class="text-secondary">Tracks</small>
|
||||
</div>
|
||||
<div class="creative-stat-box">
|
||||
<div class="creative-stat-val">{{ music_count }}</div>
|
||||
<div class="creative-stat-label">TRACKS</div>
|
||||
</div>
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body text-center py-2 px-3">
|
||||
<h4 class="mb-0">{{ video_count }}</h4>
|
||||
<small class="text-secondary">Clips</small>
|
||||
</div>
|
||||
<div class="creative-stat-box">
|
||||
<div class="creative-stat-val">{{ video_count }}</div>
|
||||
<div class="creative-stat-label">CLIPS</div>
|
||||
</div>
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body text-center py-2 px-3">
|
||||
<h4 class="mb-0">{{ project_count }}</h4>
|
||||
<small class="text-secondary">Projects</small>
|
||||
</div>
|
||||
<div class="creative-stat-box">
|
||||
<div class="creative-stat-val">{{ project_count }}</div>
|
||||
<div class="creative-stat-label">PROJECTS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<ul class="nav nav-tabs mb-4" role="tablist">
|
||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-images" type="button">Images</button>
|
||||
</li>
|
||||
@@ -56,143 +118,149 @@
|
||||
<div class="tab-content">
|
||||
<!-- Images Tab -->
|
||||
<div class="tab-pane fade show active" id="tab-images" role="tabpanel">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header">
|
||||
<strong>Pixel</strong> — Visual Architect (FLUX)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-2">Generate images by sending a task to the swarm: <code>"Generate an image of ..."</code></p>
|
||||
<p class="text-secondary small">Tools: <span class="badge bg-primary">generate_image</span> <span class="badge bg-primary">generate_storyboard</span> <span class="badge bg-primary">image_variations</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mc-panel persona-card mb-3">
|
||||
<div class="card-header mc-panel-header">
|
||||
<strong>Pixel</strong> — Visual Architect (FLUX)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Generate images by sending a task to the swarm: <code>"Generate an image of ..."</code></p>
|
||||
<p>Tools: <span class="badge badge-info">generate_image</span> <span class="badge badge-info">generate_storyboard</span> <span class="badge badge-info">image_variations</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% if images %}
|
||||
<div class="row g-3">
|
||||
{% for img in images %}
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-dark border-secondary h-100">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<small class="text-secondary">{{ img.name }}</small>
|
||||
<small style="color:var(--text-dim);">{{ img.name }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary">No images generated yet. Send an image generation task to the swarm to get started.</div>
|
||||
<div style="text-align:center; padding:24px; color:var(--text-dim); font-size:0.85rem;">No images generated yet. Send an image generation task to the swarm.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Music Tab -->
|
||||
<div class="tab-pane fade" id="tab-music" role="tabpanel">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header">
|
||||
<strong>Lyra</strong> — Sound Weaver (ACE-Step 1.5)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-2">Generate music by sending a task: <code>"Compose a pop song about ..."</code></p>
|
||||
<p class="text-secondary small">Tools: <span class="badge bg-success">generate_song</span> <span class="badge bg-success">generate_instrumental</span> <span class="badge bg-success">generate_vocals</span> <span class="badge bg-success">list_genres</span></p>
|
||||
<p class="text-secondary small mb-0">Genres: pop, rock, hip-hop, r&b, jazz, blues, country, electronic, classical, folk, reggae, metal, punk, soul, funk, latin, ambient, lo-fi, cinematic</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mc-panel persona-card mb-3">
|
||||
<div class="card-header mc-panel-header">
|
||||
<strong>Lyra</strong> — Sound Weaver (ACE-Step 1.5)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Generate music by sending a task: <code>"Compose a pop song about ..."</code></p>
|
||||
<p>Tools: <span class="badge badge-success">generate_song</span> <span class="badge badge-success">generate_instrumental</span> <span class="badge badge-success">generate_vocals</span> <span class="badge badge-success">list_genres</span></p>
|
||||
<p style="margin-bottom:0;">Genres: pop, rock, hip-hop, r&b, jazz, blues, country, electronic, classical, folk, reggae, metal, punk, soul, funk, latin, ambient, lo-fi, cinematic</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if music_files %}
|
||||
<div class="list-group">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{% for track in music_files %}
|
||||
<div class="list-group-item bg-dark border-secondary d-flex justify-content-between align-items-center">
|
||||
<span>{{ track.name }}</span>
|
||||
<audio controls preload="none"><source src="/static/{{ track.path }}" type="audio/wav"></audio>
|
||||
<div class="card">
|
||||
<div class="card-body d-flex justify-content-between align-items-center flex-wrap gap-2" style="padding:10px 14px;">
|
||||
<span style="color:var(--text-bright); font-size:0.85rem;">{{ track.name }}</span>
|
||||
<audio controls preload="none" style="max-width:100%;"><source src="/static/{{ track.path }}" type="audio/wav"></audio>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary">No music tracks generated yet. Send a music generation task to the swarm.</div>
|
||||
<div style="text-align:center; padding:24px; color:var(--text-dim); font-size:0.85rem;">No music tracks generated yet. Send a music generation task to the swarm.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Video Tab -->
|
||||
<div class="tab-pane fade" id="tab-video" role="tabpanel">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header">
|
||||
<strong>Reel</strong> — Motion Director (Wan 2.1)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-2">Generate video clips: <code>"Create a cinematic clip of ..."</code></p>
|
||||
<p class="text-secondary small">Tools: <span class="badge bg-warning text-dark">generate_video_clip</span> <span class="badge bg-warning text-dark">image_to_video</span> <span class="badge bg-warning text-dark">stitch_clips</span> <span class="badge bg-warning text-dark">overlay_audio</span></p>
|
||||
<p class="text-secondary small mb-0">Resolutions: 480p, 720p | Styles: cinematic, anime, documentary, abstract, timelapse, slow-motion, music-video, vlog</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mc-panel persona-card mb-3">
|
||||
<div class="card-header mc-panel-header">
|
||||
<strong>Reel</strong> — Motion Director (Wan 2.1)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Generate video clips: <code>"Create a cinematic clip of ..."</code></p>
|
||||
<p>Tools: <span class="badge badge-warning">generate_video_clip</span> <span class="badge badge-warning">image_to_video</span> <span class="badge badge-warning">stitch_clips</span> <span class="badge badge-warning">overlay_audio</span></p>
|
||||
<p style="margin-bottom:0;">Resolutions: 480p, 720p | Styles: cinematic, anime, documentary, abstract, timelapse, slow-motion, music-video, vlog</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if videos %}
|
||||
<div class="row g-3">
|
||||
{% for vid in videos %}
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<small class="text-secondary">{{ vid.name }}</small>
|
||||
<small style="color:var(--text-dim);">{{ vid.name }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary">No video clips generated yet. Send a video generation task to the swarm.</div>
|
||||
<div style="text-align:center; padding:24px; color:var(--text-dim); font-size:0.85rem;">No video clips generated yet. Send a video generation task to the swarm.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Director Tab -->
|
||||
<div class="tab-pane fade" id="tab-director" role="tabpanel">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header">
|
||||
<strong>Creative Director</strong> — Full Pipeline
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-2">Orchestrate all three creative personas to produce a 3+ minute music video or cinematic short.</p>
|
||||
<p class="text-secondary small">Pipeline: <span class="badge bg-info">Script</span> → <span class="badge bg-primary">Storyboard</span> → <span class="badge bg-success">Music</span> → <span class="badge bg-warning text-dark">Video</span> → <span class="badge bg-danger">Assembly</span></p>
|
||||
<p class="text-secondary small mb-0">Tools: <span class="badge bg-secondary">create_project</span> <span class="badge bg-secondary">run_storyboard</span> <span class="badge bg-secondary">run_music</span> <span class="badge bg-secondary">run_video_generation</span> <span class="badge bg-secondary">run_assembly</span> <span class="badge bg-secondary">run_full_pipeline</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mc-panel persona-card mb-3">
|
||||
<div class="card-header mc-panel-header">
|
||||
<strong>Creative Director</strong> — Full Pipeline
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Orchestrate all three creative personas to produce a 3+ minute music video or cinematic short.</p>
|
||||
<p>Pipeline:
|
||||
<span class="badge badge-info">Script</span> →
|
||||
<span class="badge badge-info">Storyboard</span> →
|
||||
<span class="badge badge-success">Music</span> →
|
||||
<span class="badge badge-warning">Video</span> →
|
||||
<span class="badge badge-danger">Assembly</span>
|
||||
</p>
|
||||
<p style="margin-bottom:0;">Tools:
|
||||
<span class="badge badge-secondary">create_project</span>
|
||||
<span class="badge badge-secondary">run_storyboard</span>
|
||||
<span class="badge badge-secondary">run_music</span>
|
||||
<span class="badge badge-secondary">run_video_generation</span>
|
||||
<span class="badge badge-secondary">run_assembly</span>
|
||||
<span class="badge badge-secondary">run_full_pipeline</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3">Projects</h5>
|
||||
{% if projects %}
|
||||
<div class="row g-3">
|
||||
{% for proj in projects %}
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<strong>{{ proj.title or proj.id }}</strong>
|
||||
<span class="badge {% if proj.status == 'complete' %}bg-success{% elif proj.status == 'failed' %}bg-danger{% else %}bg-info{% endif %}">{{ proj.status }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-3 small text-secondary">
|
||||
<span>Scenes: {{ proj.scene_count }}</span>
|
||||
<span>Storyboard: {{ 'Yes' if proj.has_storyboard else 'No' }}</span>
|
||||
<span>Music: {{ 'Yes' if proj.has_music else 'No' }}</span>
|
||||
<span>Clips: {{ proj.clip_count }}</span>
|
||||
<span>Final: {{ 'Yes' if proj.has_final else 'No' }}</span>
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// PROJECTS</div>
|
||||
<div class="card-body">
|
||||
{% if projects %}
|
||||
<div class="row g-3">
|
||||
{% for proj in projects %}
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<strong style="color:var(--text-bright);">{{ proj.title or proj.id }}</strong>
|
||||
<span class="badge {% if proj.status == 'complete' %}badge-success{% elif proj.status == 'failed' %}badge-danger{% else %}badge-info{% endif %}">{{ proj.status }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-3 flex-wrap" style="font-size:0.8rem; color:var(--text-dim);">
|
||||
<span>Scenes: {{ proj.scene_count }}</span>
|
||||
<span>Storyboard: {{ 'Yes' if proj.has_storyboard else 'No' }}</span>
|
||||
<span>Music: {{ 'Yes' if proj.has_music else 'No' }}</span>
|
||||
<span>Clips: {{ proj.clip_count }}</span>
|
||||
<span>Final: {{ 'Yes' if proj.has_final else 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="text-align:center; padding:20px; color:var(--text-dim); font-size:0.85rem;">
|
||||
No creative projects yet. Use the swarm to create one: <code>"Create a music video about sunrise over mountains"</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary">No creative projects yet. Use the swarm to create one: <code>"Create a music video about sunrise over mountains"</code></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,105 +2,160 @@
|
||||
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Agent Marketplace</h2>
|
||||
<p style="color: var(--text-secondary);">Hire agents with Bitcoin. Lowest bid wins.</p>
|
||||
<div style="margin-top: 8px; font-size: 0.875rem; color: var(--text-muted);">
|
||||
<span style="color: var(--success);">{{ active_count }}</span> active
|
||||
·
|
||||
<span style="color: var(--text-muted);">{{ planned_count }}</span> planned
|
||||
</div>
|
||||
</div>
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.market-container { max-width: 1000px; margin: 0 auto; }
|
||||
|
||||
{% if agents %}
|
||||
.market-header {
|
||||
border-left: 3px solid var(--orange);
|
||||
padding-left: 1rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.market-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.market-subtitle { font-size: 0.8rem; color: var(--text-dim); margin-top: 4px; }
|
||||
.market-stats { font-size: 0.8rem; color: var(--text-dim); margin-top: 6px; }
|
||||
.market-stats .up { color: var(--green); }
|
||||
|
||||
.market-agent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(24, 10, 45, 0.6);
|
||||
margin-bottom: 10px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.market-agent:hover { border-color: rgba(124, 58, 237, 0.3); }
|
||||
|
||||
.market-agent-price {
|
||||
text-align: right;
|
||||
min-width: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.price-amount {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--orange);
|
||||
font-family: var(--font);
|
||||
}
|
||||
.price-label { font-size: 0.7rem; color: var(--text-dim); }
|
||||
.price-stat { font-size: 0.8rem; color: var(--text-dim); margin-top: 2px; }
|
||||
.price-stat .earned { color: var(--green); }
|
||||
|
||||
.how-step {
|
||||
text-align: center;
|
||||
padding: 20px 12px;
|
||||
}
|
||||
.how-step-num {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--purple);
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--font);
|
||||
}
|
||||
.how-step h3 { font-size: 0.9rem; color: var(--text-bright); margin-bottom: 6px; }
|
||||
.how-step p { font-size: 0.8rem; color: var(--text-dim); line-height: 1.5; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.market-title { font-size: 1.1rem; }
|
||||
.market-agent { flex-direction: column; gap: 10px; }
|
||||
.market-agent-price { text-align: left; min-width: unset; }
|
||||
.price-amount { font-size: 1.1rem; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="market-container py-3">
|
||||
|
||||
<div class="market-header">
|
||||
<div class="market-title">AGENT MARKETPLACE</div>
|
||||
<div class="market-subtitle">Hire agents with Bitcoin. Lowest bid wins.</div>
|
||||
<div class="market-stats">
|
||||
<span class="up">{{ active_count }}</span> active ·
|
||||
<span>{{ planned_count }}</span> planned
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// AVAILABLE AGENTS</div>
|
||||
<div class="card-body">
|
||||
{% if agents %}
|
||||
{% for agent in agents %}
|
||||
<div class="agent-card">
|
||||
<div class="agent-avatar">{{ agent.name[0] }}</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">
|
||||
{{ agent.name | e }}
|
||||
<span style="font-size: 0.75rem; font-weight: 400;
|
||||
color: var(--text-muted); margin-left: 8px;">
|
||||
{{ agent.role | e }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="agent-meta">{{ (agent.description or 'No description') | e }}</div>
|
||||
<div class="agent-meta" style="margin-top: 4px;">
|
||||
<span class="badge badge-{{ 'success' if agent.status == 'idle'
|
||||
else 'warning' if agent.status == 'busy'
|
||||
else 'danger' if agent.status == 'offline'
|
||||
else 'secondary' }}">
|
||||
{{ agent.status }}
|
||||
</span>
|
||||
{% if agent.capabilities %}
|
||||
{% for cap in agent.capabilities.split(',') %}
|
||||
<span class="badge badge-secondary" style="margin-left: 4px;">{{ cap.strip() | e }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="market-agent">
|
||||
<div class="agent-avatar">{{ agent.name[0] }}</div>
|
||||
<div class="agent-info" style="flex:1; min-width:0;">
|
||||
<div class="agent-name">
|
||||
{{ agent.name | e }}
|
||||
<span style="font-size:0.7rem; font-weight:400; color:var(--text-dim); margin-left:6px;">{{ agent.role | e }}</span>
|
||||
</div>
|
||||
<div style="text-align: right; min-width: 120px;">
|
||||
<div style="font-size: 1.5rem; font-weight: bold; color: var(--accent);">
|
||||
{% if agent.rate_sats == 0 %}
|
||||
FREE
|
||||
{% else %}
|
||||
{{ agent.rate_sats }} sats
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);">
|
||||
min bid
|
||||
</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-top: 4px;">
|
||||
{{ agent.tasks_completed }} tasks won
|
||||
</div>
|
||||
<div style="font-size: 0.875rem; color: var(--success);">
|
||||
{{ agent.total_earned }} sats earned
|
||||
</div>
|
||||
<div class="agent-meta">{{ (agent.description or 'No description') | e }}</div>
|
||||
<div class="agent-meta" style="margin-top:4px; display:flex; gap:4px; flex-wrap:wrap;">
|
||||
<span class="badge badge-{{ 'success' if agent.status == 'idle' else 'warning' if agent.status == 'busy' else 'danger' if agent.status == 'offline' else 'secondary' }}">
|
||||
{{ agent.status }}
|
||||
</span>
|
||||
{% if agent.capabilities %}
|
||||
{% for cap in agent.capabilities.split(',') %}
|
||||
<span class="badge badge-secondary">{{ cap.strip() | e }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="market-agent-price">
|
||||
<div class="price-amount">{% if agent.rate_sats == 0 %}FREE{% else %}{{ agent.rate_sats }} sats{% endif %}</div>
|
||||
<div class="price-label">min bid</div>
|
||||
<div class="price-stat">{{ agent.tasks_completed }} tasks won</div>
|
||||
<div class="price-stat"><span class="earned">{{ agent.total_earned }} sats</span> earned</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div style="text-align: center; padding: 40px;">
|
||||
<p style="color: var(--text-muted); margin-bottom: 20px;">No agents in the marketplace yet.</p>
|
||||
<a href="/agents/timmy/chat" class="btn btn-primary">Launch Your First Agent</a>
|
||||
{% else %}
|
||||
<div style="text-align:center; padding:30px; color:var(--text-dim); font-size:12px; letter-spacing:0.08em;">
|
||||
NO AGENTS IN THE MARKETPLACE YET
|
||||
<div style="margin-top:12px;"><a href="/" class="btn btn-primary btn-sm">Launch Your First Agent</a></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">How It Works</h2>
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// HOW IT WORKS</div>
|
||||
<div class="card-body">
|
||||
<div class="grid grid-3">
|
||||
<div class="how-step">
|
||||
<div class="how-step-num">1</div>
|
||||
<h3>Post a Task</h3>
|
||||
<p>Describe what you need done at <a href="/swarm/live">/swarm/live</a></p>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<div class="how-step-num">2</div>
|
||||
<h3>Agents Bid</h3>
|
||||
<p>15-second auction — lowest bid wins</p>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<div class="how-step-num">3</div>
|
||||
<h3>Pay in Sats</h3>
|
||||
<p>Lightning payment to winning agent</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-3">
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<div style="font-size: 2rem; margin-bottom: 12px;">1</div>
|
||||
<h3 style="margin-bottom: 8px;">Post a Task</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.875rem;">
|
||||
Describe what you need done at <a href="/swarm/live">/swarm/live</a>
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<div style="font-size: 2rem; margin-bottom: 12px;">2</div>
|
||||
<h3 style="margin-bottom: 8px;">Agents Bid</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.875rem;">15-second auction — lowest bid wins</p>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<div style="font-size: 2rem; margin-bottom: 12px;">3</div>
|
||||
<h3 style="margin-bottom: 8px;">Pay in Sats</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.875rem;">Lightning payment to winning agent</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Spawn a Persona</h2>
|
||||
<p style="color: var(--text-secondary); font-size: 0.875rem;">
|
||||
Add a built-in persona agent to the live swarm from the
|
||||
<a href="/swarm/live">Swarm Live</a> dashboard.
|
||||
</p>
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// SPAWN A PERSONA</div>
|
||||
<div class="card-body">
|
||||
<p style="color:var(--text-dim); font-size:0.85rem;">
|
||||
Add a built-in persona agent to the live swarm from the <a href="/swarm/live">Swarm Live</a> dashboard.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,205 +4,277 @@
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.mobile-only { display: none; }
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 12px 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.mobile-nav a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mobile-nav a.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.mobile-nav-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.touch-button {
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.mobile-only { display: none; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.desktop-message { display: none; }
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 52px;
|
||||
border-radius: var(--radius-md);
|
||||
font-family: var(--font);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-decoration: none;
|
||||
color: var(--text-bright);
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(24, 10, 45, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
transition: transform 0.1s, border-color 0.2s, box-shadow 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.quick-btn:hover { color: var(--text-bright); text-decoration: none; }
|
||||
.quick-btn:active { transform: scale(0.96); }
|
||||
.quick-btn.voice {
|
||||
border-color: var(--border-glow);
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
.quick-btn.voice:active {
|
||||
box-shadow: 0 0 18px rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
.mobile-chat-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.mobile-chat-log {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 14px;
|
||||
max-height: 300px;
|
||||
}
|
||||
.mobile-chat-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
padding-bottom: max(10px, env(safe-area-inset-bottom));
|
||||
background: rgba(24, 10, 45, 0.9);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.mobile-chat-input input {
|
||||
flex: 1;
|
||||
background: rgba(8, 4, 18, 0.75);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-bright);
|
||||
font-family: var(--font);
|
||||
font-size: 16px;
|
||||
padding: 10px 12px;
|
||||
min-height: 44px;
|
||||
}
|
||||
.mobile-chat-input input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-glow);
|
||||
box-shadow: 0 0 0 1px var(--border-glow), 0 0 8px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
.mobile-chat-input input::placeholder { color: var(--text-dim); }
|
||||
.mobile-chat-input button {
|
||||
background: var(--border-glow);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-bright);
|
||||
font-family: var(--font);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 0 16px;
|
||||
min-height: 44px;
|
||||
letter-spacing: 0.1em;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.mobile-chat-input button:active { transform: scale(0.96); }
|
||||
|
||||
.mobile-agents-list {
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-chat-msg {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.mobile-chat-msg .meta {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.mobile-chat-msg.user .meta { color: var(--orange); }
|
||||
.mobile-chat-msg.timmy .meta { color: var(--purple); }
|
||||
.mobile-chat-msg .bubble {
|
||||
background: rgba(24, 10, 45, 0.8);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
}
|
||||
.mobile-chat-msg.timmy .bubble {
|
||||
border-left: 3px solid var(--purple);
|
||||
}
|
||||
.mobile-chat-msg.user .bubble {
|
||||
border-color: var(--border-glow);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mobile-only">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">🎙️ Quick Actions</h2>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
|
||||
<a href="/voice/button" class="btn btn-primary touch-button">
|
||||
🎤 Voice
|
||||
</a>
|
||||
<a href="/swarm/task/create" class="btn btn-secondary touch-button">
|
||||
➕ Task
|
||||
</a>
|
||||
<a href="/swarm/live" class="btn btn-secondary touch-button">
|
||||
📊 Swarm
|
||||
</a>
|
||||
<a href="/marketplace" class="btn btn-secondary touch-button">
|
||||
🏪 Market
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// QUICK ACTIONS</div>
|
||||
<div class="quick-grid">
|
||||
<a href="/voice/button" class="quick-btn voice">🎤 Voice</a>
|
||||
<a href="/swarm/task/create" class="quick-btn">➕ Task</a>
|
||||
<a href="/swarm/live" class="quick-btn">📊 Swarm</a>
|
||||
<a href="/marketplace/ui" class="quick-btn">🏪 Market</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">💬 Chat with Timmy</h2>
|
||||
</div>
|
||||
<div id="mobile-chat" class="chat-container" style="height: 300px;">
|
||||
<div class="chat-message timmy">
|
||||
<div class="chat-meta">Timmy</div>
|
||||
<div>Sir, Timmy here. Ready for your command.</div>
|
||||
</div>
|
||||
</div>
|
||||
<form onsubmit="sendMobileMessage(event)">
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<input type="text" id="mobile-message" placeholder="Message Timmy..."
|
||||
style="flex: 1;" required autocomplete="off">
|
||||
<button type="submit" class="btn btn-primary touch-button">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Chat -->
|
||||
<div class="card mc-panel mobile-chat-wrap">
|
||||
<div class="card-header mc-panel-header">// TIMMY</div>
|
||||
<div class="mobile-chat-log" id="mobile-chat">
|
||||
<div class="mobile-chat-msg timmy">
|
||||
<div class="meta">TIMMY</div>
|
||||
<div class="bubble">Sir, Timmy here. Ready for your command.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">🤖 Your Agents</h2>
|
||||
</div>
|
||||
{% if agents %}
|
||||
{% for agent in agents[:3] %}
|
||||
<div class="agent-card">
|
||||
<div class="agent-avatar">{{ agent.name[0] }}</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">{{ agent.name }}</div>
|
||||
<div class="agent-meta">
|
||||
<span class="badge badge-{{ 'success' if agent.status == 'active' else 'warning' if agent.status == 'busy' else 'danger' }}">
|
||||
{{ agent.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form onsubmit="sendMobileMessage(event)" class="mobile-chat-input">
|
||||
<input type="text" id="mobile-message" placeholder="Message Timmy..." required autocomplete="off" />
|
||||
<button type="submit">SEND</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Agents -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// YOUR AGENTS</div>
|
||||
<div class="mobile-agents-list">
|
||||
{% if agents %}
|
||||
{% for agent in agents[:3] %}
|
||||
<div class="agent-card">
|
||||
<div class="agent-avatar">{{ agent.name[0] }}</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-name">{{ agent.name }}</div>
|
||||
<div class="agent-meta">
|
||||
<span class="badge badge-{{ 'success' if agent.status == 'active' else 'warning' if agent.status == 'busy' else 'danger' }}">
|
||||
{{ agent.status }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color: var(--text-muted); text-align: center; padding: 20px;">
|
||||
No agents yet. Launch one from Mission Control.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div style="color: var(--text-dim); text-align: center; padding: 20px; font-size: 12px; letter-spacing: 0.08em;">
|
||||
NO AGENTS YET
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<nav class="mobile-nav">
|
||||
<a href="/mobile" class="active">
|
||||
<span class="mobile-nav-icon">🏠</span>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="/agents/timmy/chat">
|
||||
<span class="mobile-nav-icon">💬</span>
|
||||
<span>Chat</span>
|
||||
</a>
|
||||
<a href="/swarm/live">
|
||||
<span class="mobile-nav-icon">📊</span>
|
||||
<span>Swarm</span>
|
||||
</a>
|
||||
<a href="/marketplace">
|
||||
<span class="mobile-nav-icon">🏪</span>
|
||||
<span>Market</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div style="height: 80px;"></div> <!-- Spacer for bottom nav -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Desktop fallback -->
|
||||
<div class="desktop-message" style="text-align: center; padding: 60px;">
|
||||
<p style="font-size: 3rem; margin-bottom: 20px;">📱</p>
|
||||
<h2 style="margin-bottom: 16px;">Mobile Dashboard</h2>
|
||||
<p style="color: var(--text-secondary);">
|
||||
This page is optimized for mobile devices.<br>
|
||||
Please visit on your iPhone or use the desktop dashboard.
|
||||
</p>
|
||||
<a href="/agents/timmy/chat" class="btn btn-primary" style="margin-top: 20px;">
|
||||
Go to Desktop Dashboard
|
||||
</a>
|
||||
<p style="font-size: 3rem; margin-bottom: 20px;">📱</p>
|
||||
<h2 style="margin-bottom: 16px; color: var(--text-bright);">Mobile Dashboard</h2>
|
||||
<p style="color: var(--text-dim);">
|
||||
This page is optimized for mobile devices.<br>
|
||||
Please visit on your iPhone or use the desktop dashboard.
|
||||
</p>
|
||||
<a href="/" class="btn btn-primary" style="margin-top: 20px;">
|
||||
Go to Desktop Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple mobile chat
|
||||
async function sendMobileMessage(event) {
|
||||
event.preventDefault();
|
||||
const input = document.getElementById('mobile-message');
|
||||
const message = input.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
|
||||
const chat = document.getElementById('mobile-chat');
|
||||
|
||||
// Add user message — use DOM methods to avoid XSS
|
||||
|
||||
// Add user message
|
||||
const userDiv = document.createElement('div');
|
||||
userDiv.className = 'chat-message user';
|
||||
userDiv.className = 'mobile-chat-msg user';
|
||||
const userMeta = document.createElement('div');
|
||||
userMeta.className = 'chat-meta';
|
||||
userMeta.textContent = 'You';
|
||||
const userText = document.createElement('div');
|
||||
userText.textContent = message; // textContent escapes HTML
|
||||
userMeta.className = 'meta';
|
||||
userMeta.textContent = 'YOU';
|
||||
const userBubble = document.createElement('div');
|
||||
userBubble.className = 'bubble';
|
||||
userBubble.textContent = message;
|
||||
userDiv.appendChild(userMeta);
|
||||
userDiv.appendChild(userText);
|
||||
userDiv.appendChild(userBubble);
|
||||
chat.appendChild(userDiv);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
|
||||
|
||||
input.value = '';
|
||||
|
||||
// Send to server
|
||||
|
||||
try {
|
||||
const response = await fetch('/agents/timmy/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `message=${encodeURIComponent(message)}`
|
||||
body: 'message=' + encodeURIComponent(message)
|
||||
});
|
||||
const html = await response.text();
|
||||
|
||||
// Extract Timmy's response from HTML
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const timmyResponse = doc.querySelector('.chat-message.timmy');
|
||||
|
||||
if (timmyResponse) {
|
||||
chat.appendChild(timmyResponse.cloneNode(true));
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
const timmyResponse = doc.querySelector('.chat-message.timmy, .msg-body');
|
||||
|
||||
const replyDiv = document.createElement('div');
|
||||
replyDiv.className = 'mobile-chat-msg timmy';
|
||||
const replyMeta = document.createElement('div');
|
||||
replyMeta.className = 'meta';
|
||||
replyMeta.textContent = 'TIMMY';
|
||||
const replyBubble = document.createElement('div');
|
||||
replyBubble.className = 'bubble';
|
||||
replyBubble.textContent = timmyResponse ? timmyResponse.textContent.trim() : 'Response received.';
|
||||
replyDiv.appendChild(replyMeta);
|
||||
replyDiv.appendChild(replyBubble);
|
||||
chat.appendChild(replyDiv);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
} catch (e) {
|
||||
const errDiv = document.createElement('div');
|
||||
errDiv.className = 'chat-message timmy';
|
||||
errDiv.className = 'mobile-chat-msg timmy';
|
||||
const errMeta = document.createElement('div');
|
||||
errMeta.className = 'chat-meta';
|
||||
errMeta.textContent = 'Timmy';
|
||||
const errText = document.createElement('div');
|
||||
errText.style.color = 'var(--danger)';
|
||||
errText.textContent = 'Sorry, I could not process that. Try again?';
|
||||
errMeta.className = 'meta';
|
||||
errMeta.textContent = 'TIMMY';
|
||||
const errBubble = document.createElement('div');
|
||||
errBubble.className = 'bubble';
|
||||
errBubble.style.color = 'var(--red)';
|
||||
errBubble.textContent = 'Sorry, I could not process that. Try again?';
|
||||
errDiv.appendChild(errMeta);
|
||||
errDiv.appendChild(errText);
|
||||
errDiv.appendChild(errBubble);
|
||||
chat.appendChild(errDiv);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,295 @@
|
||||
|
||||
{% block title %}Timmy Time — Spark Intelligence{% endblock %}
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
/* ── Spark Intelligence — unified theme ── */
|
||||
|
||||
.spark-container { max-width: 1400px; margin: 0 auto; }
|
||||
|
||||
.spark-header {
|
||||
border-left: 3px solid var(--purple);
|
||||
padding-left: 1rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.spark-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--purple);
|
||||
letter-spacing: 0.08em;
|
||||
font-family: var(--font);
|
||||
}
|
||||
.spark-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.spark-status-val {
|
||||
color: var(--purple);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Stat grid */
|
||||
.spark-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.spark-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(8, 4, 18, 0.5);
|
||||
}
|
||||
.spark-stat-label {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.spark-stat-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
/* Event pipeline rows */
|
||||
.spark-event-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.spark-event-row:last-child { border-bottom: none; }
|
||||
.spark-event-count {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
/* Event type badges */
|
||||
.spark-event-type-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.15em 0.5em;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
background: rgba(59, 26, 92, 0.4);
|
||||
color: var(--text);
|
||||
}
|
||||
.spark-type-task_posted .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-task_posted { background: rgba(124, 58, 237, 0.2); color: var(--purple); }
|
||||
.spark-type-bid_submitted .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-bid_submitted { background: rgba(255, 122, 42, 0.2); color: var(--orange); }
|
||||
.spark-type-task_assigned .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-task_assigned { background: rgba(0, 232, 122, 0.15); color: var(--green); }
|
||||
.spark-type-task_completed .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-task_completed { background: rgba(0, 232, 122, 0.2); color: var(--green); }
|
||||
.spark-type-task_failed .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-task_failed { background: rgba(255, 68, 85, 0.2); color: var(--red); }
|
||||
.spark-type-agent_joined .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-agent_joined { background: rgba(168, 85, 247, 0.2); color: var(--purple); }
|
||||
.spark-type-prediction_result .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-prediction_result { background: rgba(168, 85, 247, 0.15); color: #c084fc; }
|
||||
|
||||
/* Advisories */
|
||||
.spark-advisory {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: rgba(24, 10, 45, 0.5);
|
||||
}
|
||||
.spark-advisory.priority-high { border-left: 3px solid var(--red); }
|
||||
.spark-advisory.priority-medium { border-left: 3px solid var(--orange); }
|
||||
.spark-advisory.priority-low { border-left: 3px solid var(--green); }
|
||||
|
||||
.spark-advisory-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.spark-advisory-cat {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.spark-advisory-priority {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
}
|
||||
.spark-advisory-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-bright);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.spark-advisory-detail {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.spark-advisory-action {
|
||||
font-size: 0.75rem;
|
||||
color: var(--purple);
|
||||
font-style: italic;
|
||||
border-left: 2px solid var(--purple);
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Predictions */
|
||||
.spark-prediction {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.6rem;
|
||||
margin-bottom: 0.6rem;
|
||||
background: rgba(8, 4, 18, 0.5);
|
||||
}
|
||||
.spark-prediction.evaluated { border-left: 3px solid var(--green); }
|
||||
.spark-prediction.pending { border-left: 3px solid var(--orange); }
|
||||
|
||||
.spark-pred-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.spark-pred-task {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
}
|
||||
.spark-pred-accuracy {
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--font);
|
||||
}
|
||||
.spark-pred-pending-badge {
|
||||
font-size: 0.6rem;
|
||||
background: var(--amber-dim);
|
||||
color: var(--amber);
|
||||
padding: 0.1em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.spark-pred-detail { font-size: 0.75rem; color: var(--text); }
|
||||
.spark-pred-item { padding: 0.1rem 0; }
|
||||
.spark-pred-label { color: var(--text-dim); font-weight: 600; }
|
||||
.spark-pred-actual {
|
||||
margin-top: 0.3rem;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px dashed var(--border);
|
||||
color: var(--text-bright);
|
||||
}
|
||||
.spark-pred-time {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 0.3rem;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
/* Memories */
|
||||
.spark-memory-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.6rem;
|
||||
margin-bottom: 0.6rem;
|
||||
background: rgba(8, 4, 18, 0.5);
|
||||
}
|
||||
.spark-memory-card.mem-pattern { border-left: 3px solid var(--green); }
|
||||
.spark-memory-card.mem-anomaly { border-left: 3px solid var(--red); }
|
||||
.spark-memory-card.mem-insight { border-left: 3px solid var(--purple); }
|
||||
|
||||
.spark-mem-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.spark-mem-type {
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-dim);
|
||||
font-weight: 600;
|
||||
}
|
||||
.spark-mem-confidence {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
}
|
||||
.spark-mem-content {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-bright);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.spark-mem-meta {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
.spark-timeline-scroll {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.spark-event {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: rgba(8, 4, 18, 0.5);
|
||||
}
|
||||
.spark-event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.spark-event-importance {
|
||||
font-size: 0.5rem;
|
||||
color: var(--purple);
|
||||
}
|
||||
.spark-event-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
.spark-event-meta {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--font);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
.spark-event-time {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.spark-title { font-size: 1.1rem; }
|
||||
.spark-stat-value { font-size: 1.1rem; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.spark-timeline-scroll { max-height: 50vh; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid spark-container py-4">
|
||||
<div class="container-fluid spark-container py-3">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="spark-header mb-4">
|
||||
<div class="spark-header">
|
||||
<div class="spark-title">SPARK INTELLIGENCE</div>
|
||||
<div class="spark-subtitle">
|
||||
Self-evolving cognitive layer —
|
||||
@@ -21,7 +305,6 @@
|
||||
<!-- Left column: Status + Advisories -->
|
||||
<div class="col-12 col-lg-4 d-flex flex-column gap-3">
|
||||
|
||||
<!-- EIDOS Status -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// EIDOS LOOP</div>
|
||||
<div class="card-body p-3">
|
||||
@@ -48,7 +331,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Counts -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// EVENT PIPELINE</div>
|
||||
<div class="card-body p-3">
|
||||
@@ -61,7 +343,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advisories -->
|
||||
<div class="card mc-panel"
|
||||
hx-get="/spark/insights"
|
||||
hx-trigger="load, every 30s"
|
||||
@@ -69,7 +350,7 @@
|
||||
hx-swap="innerHTML">
|
||||
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
|
||||
<span>// ADVISORIES</span>
|
||||
<span class="badge bg-info">{{ advisories | length }}</span>
|
||||
<span class="badge badge-info">{{ advisories | length }}</span>
|
||||
</div>
|
||||
<div class="card-body p-3" id="spark-insights-body">
|
||||
{% if advisories %}
|
||||
@@ -85,7 +366,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">No advisories yet. Run more tasks to build intelligence.</div>
|
||||
<div style="text-align:center; color:var(--text-dim); padding:16px; font-size:0.85rem;">No advisories yet. Run more tasks to build intelligence.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,7 +375,6 @@
|
||||
<!-- Middle column: Predictions -->
|
||||
<div class="col-12 col-lg-4 d-flex flex-column gap-3">
|
||||
|
||||
<!-- EIDOS Predictions -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// EIDOS PREDICTIONS</div>
|
||||
<div class="card-body p-3">
|
||||
@@ -139,12 +419,11 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">No predictions yet. Post tasks to activate the EIDOS loop.</div>
|
||||
<div style="text-align:center; color:var(--text-dim); padding:16px; font-size:0.85rem;">No predictions yet. Post tasks to activate the EIDOS loop.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consolidated Memories -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// MEMORIES</div>
|
||||
<div class="card-body p-3">
|
||||
@@ -162,7 +441,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">Memories will form as patterns emerge.</div>
|
||||
<div style="text-align:center; color:var(--text-dim); padding:16px; font-size:0.85rem;">Memories will form as patterns emerge.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,7 +457,7 @@
|
||||
hx-swap="innerHTML">
|
||||
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
|
||||
<span>// EVENT TIMELINE</span>
|
||||
<span class="badge bg-secondary">{{ status.events_captured }} total</span>
|
||||
<span class="badge badge-secondary">{{ status.events_captured }} total</span>
|
||||
</div>
|
||||
<div class="card-body p-3 spark-timeline-scroll" id="spark-timeline-body">
|
||||
{% if timeline %}
|
||||
@@ -198,7 +477,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">No events captured yet.</div>
|
||||
<div style="text-align:center; color:var(--text-dim); padding:16px; font-size:0.85rem;">No events captured yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,351 +485,4 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Spark Intelligence — Mission Control theme */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
.spark-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.spark-header {
|
||||
border-left: 3px solid #00d4ff;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.spark-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: #00d4ff;
|
||||
letter-spacing: 0.08em;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.spark-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.spark-status-val {
|
||||
color: #00d4ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Stat grid */
|
||||
.spark-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.spark-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #1a2a3a;
|
||||
border-radius: 4px;
|
||||
background: #0a1520;
|
||||
}
|
||||
|
||||
.spark-stat-label {
|
||||
font-size: 0.65rem;
|
||||
color: #6c757d;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.spark-stat-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #f8f9fa;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Event pipeline rows */
|
||||
.spark-event-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0;
|
||||
border-bottom: 1px solid #1a2a3a;
|
||||
}
|
||||
|
||||
.spark-event-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.spark-event-count {
|
||||
font-weight: 600;
|
||||
color: #adb5bd;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Event type badges */
|
||||
.spark-event-type-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.15em 0.5em;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.spark-type-task_posted .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-task_posted { background: #1a3a5a; color: #5baaff; }
|
||||
.spark-type-bid_submitted .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-bid_submitted { background: #3a2a1a; color: #ffaa5b; }
|
||||
.spark-type-task_assigned .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-task_assigned { background: #1a3a2a; color: #5bffaa; }
|
||||
.spark-type-task_completed .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-task_completed { background: #1a3a1a; color: #5bff5b; }
|
||||
.spark-type-task_failed .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-task_failed { background: #3a1a1a; color: #ff5b5b; }
|
||||
.spark-type-agent_joined .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-agent_joined { background: #2a1a3a; color: #aa5bff; }
|
||||
.spark-type-prediction_result .spark-event-type-badge,
|
||||
.spark-event-type-badge.spark-type-prediction_result { background: #1a2a3a; color: #00d4ff; }
|
||||
|
||||
/* Advisories */
|
||||
.spark-advisory {
|
||||
border: 1px solid #2a3a4a;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: #0d1b2a;
|
||||
}
|
||||
|
||||
.spark-advisory.priority-high {
|
||||
border-left: 3px solid #dc3545;
|
||||
}
|
||||
|
||||
.spark-advisory.priority-medium {
|
||||
border-left: 3px solid #fd7e14;
|
||||
}
|
||||
|
||||
.spark-advisory.priority-low {
|
||||
border-left: 3px solid #198754;
|
||||
}
|
||||
|
||||
.spark-advisory-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.spark-advisory-cat {
|
||||
font-size: 0.6rem;
|
||||
color: #6c757d;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.spark-advisory-priority {
|
||||
font-size: 0.65rem;
|
||||
color: #adb5bd;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.spark-advisory-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: #f8f9fa;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.spark-advisory-detail {
|
||||
font-size: 0.8rem;
|
||||
color: #adb5bd;
|
||||
margin-bottom: 0.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.spark-advisory-action {
|
||||
font-size: 0.75rem;
|
||||
color: #00d4ff;
|
||||
font-style: italic;
|
||||
border-left: 2px solid #00d4ff;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Predictions */
|
||||
.spark-prediction {
|
||||
border: 1px solid #1a2a3a;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
margin-bottom: 0.6rem;
|
||||
background: #0a1520;
|
||||
}
|
||||
|
||||
.spark-prediction.evaluated {
|
||||
border-left: 3px solid #198754;
|
||||
}
|
||||
|
||||
.spark-prediction.pending {
|
||||
border-left: 3px solid #fd7e14;
|
||||
}
|
||||
|
||||
.spark-pred-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.spark-pred-task {
|
||||
font-size: 0.75rem;
|
||||
color: #adb5bd;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.spark-pred-accuracy {
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.spark-pred-pending-badge {
|
||||
font-size: 0.6rem;
|
||||
background: #fd7e14;
|
||||
color: #fff;
|
||||
padding: 0.1em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.spark-pred-detail {
|
||||
font-size: 0.75rem;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.spark-pred-item {
|
||||
padding: 0.1rem 0;
|
||||
}
|
||||
|
||||
.spark-pred-label {
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.spark-pred-actual {
|
||||
margin-top: 0.3rem;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px dashed #1a2a3a;
|
||||
color: #dee2e6;
|
||||
}
|
||||
|
||||
.spark-pred-time {
|
||||
font-size: 0.6rem;
|
||||
color: #495057;
|
||||
margin-top: 0.3rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Memories */
|
||||
.spark-memory-card {
|
||||
border: 1px solid #1a2a3a;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
margin-bottom: 0.6rem;
|
||||
background: #0a1520;
|
||||
}
|
||||
|
||||
.spark-memory-card.mem-pattern {
|
||||
border-left: 3px solid #198754;
|
||||
}
|
||||
|
||||
.spark-memory-card.mem-anomaly {
|
||||
border-left: 3px solid #dc3545;
|
||||
}
|
||||
|
||||
.spark-memory-card.mem-insight {
|
||||
border-left: 3px solid #00d4ff;
|
||||
}
|
||||
|
||||
.spark-mem-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.spark-mem-type {
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.spark-mem-confidence {
|
||||
font-size: 0.65rem;
|
||||
color: #adb5bd;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.spark-mem-content {
|
||||
font-size: 0.8rem;
|
||||
color: #dee2e6;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.spark-mem-meta {
|
||||
font-size: 0.6rem;
|
||||
color: #495057;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
.spark-timeline-scroll {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.spark-event {
|
||||
border: 1px solid #1a2a3a;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #0a1520;
|
||||
}
|
||||
|
||||
.spark-event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.spark-event-importance {
|
||||
font-size: 0.5rem;
|
||||
color: #00d4ff;
|
||||
}
|
||||
|
||||
.spark-event-desc {
|
||||
font-size: 0.8rem;
|
||||
color: #dee2e6;
|
||||
}
|
||||
|
||||
.spark-event-meta {
|
||||
font-size: 0.65rem;
|
||||
color: #6c757d;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.spark-event-time {
|
||||
font-size: 0.6rem;
|
||||
color: #495057;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.spark-title { font-size: 1.2rem; }
|
||||
.spark-stat-value { font-size: 1.1rem; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,51 +2,89 @@
|
||||
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.swarm-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.swarm-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.swarm-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.swarm-log-box {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: rgba(24, 10, 45, 0.6);
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border);
|
||||
font-family: var(--font);
|
||||
font-size: 12px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.swarm-title { font-size: 1rem; }
|
||||
.swarm-log-box { height: 160px; font-size: 11px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">🔴 Live Swarm Dashboard</h2>
|
||||
<div>
|
||||
<span class="badge badge-success" id="connection-status">Connecting...</span>
|
||||
</div>
|
||||
<div class="swarm-container py-3">
|
||||
|
||||
<div class="swarm-header-row">
|
||||
<div class="swarm-title">LIVE SWARM</div>
|
||||
<span class="badge badge-success" id="connection-status">Connecting...</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3" style="margin-bottom: 16px;">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="stat-agents">-</div>
|
||||
<div class="stat-label">Total Agents</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3" style="margin-bottom: 20px;">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="stat-agents">-</div>
|
||||
<div class="stat-label">Total Agents</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="stat-active">-</div>
|
||||
<div class="stat-label">Active</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="stat-tasks">-</div>
|
||||
<div class="stat-label">Active Tasks</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="stat-active">-</div>
|
||||
<div class="stat-label">Active</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<h3 style="margin-bottom: 12px;">Agents</h3>
|
||||
<div id="agents-list">
|
||||
<p style="color: var(--text-muted);">Loading agents...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin-bottom: 12px;">Active Auctions</h3>
|
||||
<div id="auctions-list">
|
||||
<p style="color: var(--text-muted);">Loading auctions...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="stat-tasks">-</div>
|
||||
<div class="stat-label">Active Tasks</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<h3 style="margin-bottom: 12px;">Swarm Log</h3>
|
||||
<div id="swarm-log" style="height: 200px; overflow-y: auto; background: var(--bg-tertiary); padding: 12px; border-radius: 8px; font-family: monospace; font-size: 0.875rem;">
|
||||
<div style="color: var(--text-muted);">Waiting for updates...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2" style="margin-bottom: 16px;">
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// AGENTS</div>
|
||||
<div class="card-body" id="agents-list">
|
||||
<p style="color: var(--text-dim); font-size: 12px;">Loading agents...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// ACTIVE AUCTIONS</div>
|
||||
<div class="card-body" id="auctions-list">
|
||||
<p style="color: var(--text-dim); font-size: 12px;">Loading auctions...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// SWARM LOG</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="swarm-log-box" id="swarm-log">
|
||||
<div style="color: var(--text-dim);">Waiting for updates...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -56,8 +94,8 @@ const maxReconnectInterval = 30000;
|
||||
|
||||
function connect() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(`${protocol}//${window.location.host}/swarm/live`);
|
||||
|
||||
ws = new WebSocket(protocol + '//' + window.location.host + '/swarm/live');
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('WebSocket connected');
|
||||
document.getElementById('connection-status').textContent = 'Live';
|
||||
@@ -65,22 +103,21 @@ function connect() {
|
||||
reconnectInterval = 1000;
|
||||
addLog('Connected to swarm', 'success');
|
||||
};
|
||||
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
const message = JSON.parse(event.data);
|
||||
var message = JSON.parse(event.data);
|
||||
handleMessage(message);
|
||||
};
|
||||
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log('WebSocket disconnected');
|
||||
document.getElementById('connection-status').textContent = 'Reconnecting...';
|
||||
document.getElementById('connection-status').className = 'badge badge-warning';
|
||||
addLog('Disconnected, reconnecting...', 'warning');
|
||||
|
||||
setTimeout(connect, reconnectInterval);
|
||||
reconnectInterval = Math.min(reconnectInterval * 2, maxReconnectInterval);
|
||||
};
|
||||
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket error:', error);
|
||||
addLog('Connection error', 'error');
|
||||
@@ -88,9 +125,8 @@ function connect() {
|
||||
}
|
||||
|
||||
function handleMessage(message) {
|
||||
// Handle structured state snapshots (initial_state / state_update)
|
||||
if (message.type === 'initial_state' || message.type === 'state_update') {
|
||||
const data = message.data;
|
||||
var data = message.data;
|
||||
document.getElementById('stat-agents').textContent = data.agents.total;
|
||||
document.getElementById('stat-active').textContent = data.agents.active;
|
||||
document.getElementById('stat-tasks').textContent = data.tasks.active;
|
||||
@@ -99,9 +135,8 @@ function handleMessage(message) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle individual swarm events broadcast by ws_manager
|
||||
const evt = message.event || message.type || '';
|
||||
const data = message.data || message;
|
||||
var evt = message.event || message.type || '';
|
||||
var data = message.data || message;
|
||||
|
||||
if (evt === 'agent_joined') {
|
||||
addLog('Agent joined: ' + (data.name || data.agent_id || ''), 'success');
|
||||
@@ -124,43 +159,42 @@ function handleMessage(message) {
|
||||
}
|
||||
|
||||
function refreshStats() {
|
||||
// Fetch current swarm status via REST and update the stat counters
|
||||
fetch('/swarm').then(r => r.json()).then(data => {
|
||||
fetch('/swarm').then(function(r) { return r.json(); }).then(function(data) {
|
||||
document.getElementById('stat-agents').textContent = data.agents || 0;
|
||||
document.getElementById('stat-active').textContent = data.agents_busy || 0;
|
||||
document.getElementById('stat-tasks').textContent = (data.tasks_pending || 0) + (data.tasks_running || 0);
|
||||
}).catch(() => {});
|
||||
}).catch(function() {});
|
||||
}
|
||||
|
||||
// Safe text setter — avoids XSS when inserting user/server data into DOM
|
||||
function _t(el, text) { el.textContent = text; return el; }
|
||||
function _el(tag, cls) { const e = document.createElement(tag); if (cls) e.className = cls; return e; }
|
||||
function _el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
|
||||
|
||||
function updateAgentsList(agents) {
|
||||
const container = document.getElementById('agents-list');
|
||||
var container = document.getElementById('agents-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!agents || agents.length === 0) {
|
||||
const p = _el('p'); p.style.color = 'var(--text-muted)';
|
||||
var p = _el('p'); p.style.color = 'var(--text-dim)';
|
||||
_t(p, 'No agents registered');
|
||||
container.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
agents.forEach(agent => {
|
||||
const card = _el('div', 'agent-card');
|
||||
const avatar = _el('div', 'agent-avatar');
|
||||
agents.forEach(function(agent) {
|
||||
var card = _el('div', 'agent-card');
|
||||
var avatar = _el('div', 'agent-avatar');
|
||||
_t(avatar, (agent.name || '?').charAt(0).toUpperCase());
|
||||
const info = _el('div', 'agent-info');
|
||||
const name = _el('div', 'agent-name');
|
||||
var info = _el('div', 'agent-info');
|
||||
var name = _el('div', 'agent-name');
|
||||
_t(name, agent.name || '');
|
||||
const desc = _el('div', 'agent-meta');
|
||||
var desc = _el('div', 'agent-meta');
|
||||
_t(desc, agent.description || 'No description');
|
||||
const meta = _el('div', 'agent-meta');
|
||||
const badge = _el('span', `badge badge-${agent.status === 'active' ? 'success' : agent.status === 'busy' ? 'warning' : 'danger'}`);
|
||||
var meta = _el('div', 'agent-meta');
|
||||
var badge = _el('span', 'badge badge-' + (agent.status === 'active' ? 'success' : agent.status === 'busy' ? 'warning' : 'danger'));
|
||||
_t(badge, agent.status || '');
|
||||
const stats = _el('span');
|
||||
_t(stats, ` ${agent.min_bid ?? 0} sats min bid | ${agent.tasks_completed ?? 0} tasks | ${agent.total_earned ?? 0} sats earned`);
|
||||
var stats = _el('span');
|
||||
stats.style.marginLeft = '6px';
|
||||
_t(stats, (agent.min_bid || 0) + ' sats min | ' + (agent.tasks_completed || 0) + ' tasks | ' + (agent.total_earned || 0) + ' earned');
|
||||
meta.appendChild(badge);
|
||||
meta.appendChild(stats);
|
||||
info.appendChild(name);
|
||||
@@ -173,23 +207,23 @@ function updateAgentsList(agents) {
|
||||
}
|
||||
|
||||
function updateAuctionsList(auctions) {
|
||||
const container = document.getElementById('auctions-list');
|
||||
var container = document.getElementById('auctions-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!auctions || auctions.length === 0) {
|
||||
const p = _el('p'); p.style.color = 'var(--text-muted)';
|
||||
var p = _el('p'); p.style.color = 'var(--text-dim)';
|
||||
_t(p, 'No active auctions');
|
||||
container.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
auctions.forEach(auction => {
|
||||
const card = _el('div', 'agent-card');
|
||||
const info = _el('div', 'agent-info');
|
||||
const name = _el('div', 'agent-name');
|
||||
auctions.forEach(function(auction) {
|
||||
var card = _el('div', 'agent-card');
|
||||
var info = _el('div', 'agent-info');
|
||||
var name = _el('div', 'agent-name');
|
||||
_t(name, 'Task ' + String(auction.task_id || '').slice(0, 8));
|
||||
const meta = _el('div', 'agent-meta');
|
||||
_t(meta, `${Math.round(auction.time_remaining ?? 0)}s remaining | ${auction.bid_count ?? 0} bids`);
|
||||
var meta = _el('div', 'agent-meta');
|
||||
_t(meta, Math.round(auction.time_remaining || 0) + 's remaining | ' + (auction.bid_count || 0) + ' bids');
|
||||
info.appendChild(name);
|
||||
info.appendChild(meta);
|
||||
card.appendChild(info);
|
||||
@@ -197,27 +231,27 @@ function updateAuctionsList(auctions) {
|
||||
});
|
||||
}
|
||||
|
||||
function addLog(message, type = 'info') {
|
||||
const log = document.getElementById('swarm-log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const color = type === 'error' ? 'var(--danger)' : type === 'warning' ? 'var(--warning)' : type === 'success' ? 'var(--success)' : 'var(--text-secondary)';
|
||||
|
||||
const entry = document.createElement('div');
|
||||
function addLog(message, type) {
|
||||
type = type || 'info';
|
||||
var log = document.getElementById('swarm-log');
|
||||
var timestamp = new Date().toLocaleTimeString();
|
||||
var color = type === 'error' ? 'var(--red)' : type === 'warning' ? 'var(--amber)' : type === 'success' ? 'var(--green)' : 'var(--text-dim)';
|
||||
|
||||
var entry = document.createElement('div');
|
||||
entry.style.marginBottom = '4px';
|
||||
const tsSpan = _el('span');
|
||||
tsSpan.style.color = 'var(--text-muted)';
|
||||
var tsSpan = _el('span');
|
||||
tsSpan.style.color = 'var(--text-dim)';
|
||||
_t(tsSpan, '[' + timestamp + '] ');
|
||||
const msgSpan = _el('span');
|
||||
var msgSpan = _el('span');
|
||||
msgSpan.style.color = color;
|
||||
_t(msgSpan, message);
|
||||
entry.appendChild(tsSpan);
|
||||
entry.appendChild(msgSpan);
|
||||
|
||||
|
||||
log.appendChild(entry);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
// Connect on load
|
||||
connect();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,39 +2,97 @@
|
||||
|
||||
{% block title %}Tools & Capabilities — Mission Control{% endblock %}
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.tools-container { max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
.tools-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tools-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.tools-subtitle {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.tools-stat-box {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.tools-stat-val {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
.tools-stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
height: 100%;
|
||||
}
|
||||
.tool-card .card-title {
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.tool-card .card-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tools-title { font-size: 1.1rem; }
|
||||
.tools-header { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1 class="display-6">🔧 Tools & Capabilities</h1>
|
||||
<p class="text-secondary">Agent tools and usage statistics</p>
|
||||
<div class="tools-container py-3">
|
||||
|
||||
<div class="tools-header">
|
||||
<div>
|
||||
<div class="tools-title">TOOLS & CAPABILITIES</div>
|
||||
<div class="tools-subtitle">Agent tools and usage statistics</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="mb-0">{{ total_calls }}</h3>
|
||||
<small class="text-secondary">Total Tool Calls</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tools-stat-box">
|
||||
<div class="tools-stat-val">{{ total_calls }}</div>
|
||||
<div class="tools-stat-label">TOTAL CALLS</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Tools Reference -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">Available Tools</h5>
|
||||
<!-- Available Tools -->
|
||||
<div class="card mc-panel mb-3">
|
||||
<div class="card-header mc-panel-header">// AVAILABLE TOOLS</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{% for tool_id, tool_info in available_tools.items() %}
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 bg-dark border-secondary">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card tool-card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ tool_info.name }}</h6>
|
||||
<p class="card-text small text-secondary">{{ tool_info.description }}</p>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Available to:</small>
|
||||
<h6 class="card-title" style="color:var(--text-bright);">{{ tool_info.name }}</h6>
|
||||
<p class="card-text">{{ tool_info.description }}</p>
|
||||
<div style="margin-top:8px;">
|
||||
<small style="color:var(--text-dim); font-size:0.65rem; letter-spacing:0.08em;">AVAILABLE TO</small>
|
||||
<div class="d-flex flex-wrap gap-1 mt-1">
|
||||
{% for persona in tool_info.available_in %}
|
||||
<span class="badge bg-secondary">{{ persona|title }}</span>
|
||||
<span class="badge badge-secondary">{{ persona|title }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,37 +104,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Tool Assignments -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">Agent Capabilities</h5>
|
||||
<!-- Agent Capabilities -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// AGENT CAPABILITIES</div>
|
||||
<div class="card-body">
|
||||
{% if agent_tools %}
|
||||
<div class="row g-3">
|
||||
{% for agent in agent_tools %}
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<strong>{{ agent.name }}</strong>
|
||||
<span class="badge {% if agent.status == 'idle' %}bg-success{% elif agent.status == 'busy' %}bg-warning{% else %}bg-secondary{% endif %} ms-2">
|
||||
<strong style="color:var(--text-bright);">{{ agent.name }}</strong>
|
||||
<span class="badge {% if agent.status == 'idle' %}badge-success{% elif agent.status == 'busy' %}badge-warning{% else %}badge-secondary{% endif %} ms-2">
|
||||
{{ agent.status }}
|
||||
</span>
|
||||
</span>
|
||||
{% if agent.stats %}
|
||||
<small class="text-muted">{{ agent.stats.total_calls }} calls</small>
|
||||
<small style="color:var(--text-dim);">{{ agent.stats.total_calls }} calls</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if agent.tools %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for tool in agent.tools %}
|
||||
<span class="badge bg-primary" title="{{ tool.description }}">
|
||||
{{ tool.name }}
|
||||
</span>
|
||||
<span class="badge badge-info" title="{{ tool.description }}">{{ tool.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-secondary mb-0">No tools assigned</p>
|
||||
<p style="color:var(--text-dim); margin:0; font-size:0.85rem;">No tools assigned</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,11 +140,12 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary">
|
||||
<div style="text-align:center; padding:20px; color:var(--text-dim); font-size:0.85rem;">
|
||||
No agents registered yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,133 +4,164 @@
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.voice-button {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-dim));
|
||||
border: none;
|
||||
color: var(--bg-primary);
|
||||
font-size: 4rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40px auto;
|
||||
box-shadow: 0 0 40px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
.voice-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 60px rgba(0, 255, 136, 0.5);
|
||||
}
|
||||
|
||||
.voice-button:active, .voice-button.listening {
|
||||
transform: scale(0.95);
|
||||
background: var(--danger);
|
||||
box-shadow: 0 0 60px rgba(255, 68, 68, 0.5);
|
||||
animation: pulse-red 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-red {
|
||||
0%, 100% { box-shadow: 0 0 40px rgba(255, 68, 68, 0.5); }
|
||||
50% { box-shadow: 0 0 80px rgba(255, 68, 68, 0.8); }
|
||||
}
|
||||
|
||||
.voice-status {
|
||||
text-align: center;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.voice-result {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.voice-transcript {
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.voice-response {
|
||||
color: var(--accent);
|
||||
font-style: italic;
|
||||
}
|
||||
.voice-page {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.voice-button {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--border-glow), var(--purple));
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 3.5rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 30px auto;
|
||||
box-shadow: 0 0 40px rgba(124, 58, 237, 0.3);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.voice-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 60px rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
.voice-button:active, .voice-button.listening {
|
||||
transform: scale(0.95);
|
||||
background: linear-gradient(135deg, var(--red), var(--red-dim));
|
||||
box-shadow: 0 0 60px rgba(255, 68, 85, 0.5);
|
||||
animation: pulse-listen 1s infinite;
|
||||
}
|
||||
@keyframes pulse-listen {
|
||||
0%, 100% { box-shadow: 0 0 40px rgba(255, 68, 85, 0.5); }
|
||||
50% { box-shadow: 0 0 80px rgba(255, 68, 85, 0.8); }
|
||||
}
|
||||
|
||||
.voice-status {
|
||||
font-size: 1rem;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 16px;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.voice-result {
|
||||
background: rgba(24, 10, 45, 0.8);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
.voice-transcript {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text);
|
||||
}
|
||||
.voice-response {
|
||||
color: var(--purple);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.voice-tips {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: rgba(24, 10, 45, 0.6);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: left;
|
||||
}
|
||||
.voice-tips h3 {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-bright);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.voice-tips ul {
|
||||
color: var(--text-dim);
|
||||
line-height: 2;
|
||||
padding-left: 18px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.voice-button { width: 140px; height: 140px; font-size: 3rem; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card" style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="card-header" style="text-align: center;">
|
||||
<h2 class="card-title">🎙️ Voice Control</h2>
|
||||
<p style="color: var(--text-secondary);">Hold the button and speak to Timmy</p>
|
||||
</div>
|
||||
|
||||
<div class="voice-status" id="voice-status">Tap and hold to speak</div>
|
||||
|
||||
<button class="voice-button" id="voice-btn"
|
||||
onmousedown="startListening()"
|
||||
onmouseup="stopListening()"
|
||||
ontouchstart="startListening()"
|
||||
ontouchend="stopListening()">
|
||||
🎤
|
||||
</button>
|
||||
|
||||
<div id="voice-result" class="voice-result" style="display: none;">
|
||||
<div class="voice-page py-3">
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header" style="text-align:center;">// VOICE CONTROL</div>
|
||||
<div class="card-body">
|
||||
<p style="color: var(--text-dim); font-size: 0.85rem; margin-bottom: 0;">Hold the button and speak to Timmy</p>
|
||||
|
||||
<div class="voice-status" id="voice-status">Tap and hold to speak</div>
|
||||
|
||||
<button class="voice-button" id="voice-btn"
|
||||
onmousedown="startListening()"
|
||||
onmouseup="stopListening()"
|
||||
ontouchstart="startListening()"
|
||||
ontouchend="stopListening()">
|
||||
🎤
|
||||
</button>
|
||||
|
||||
<div id="voice-result" class="voice-result" style="display: none;">
|
||||
<div class="voice-transcript">
|
||||
<strong>You said:</strong> <span id="transcript-text"></span>
|
||||
<strong style="color:var(--text-dim);">You said:</strong> <span id="transcript-text"></span>
|
||||
</div>
|
||||
<div class="voice-response">
|
||||
<strong>Timmy:</strong> <span id="response-text"></span>
|
||||
<strong>Timmy:</strong> <span id="response-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding: 20px; background: var(--bg-tertiary); border-radius: 8px;">
|
||||
<h3 style="margin-bottom: 12px;">Try saying:</h3>
|
||||
<ul style="color: var(--text-secondary); line-height: 2;">
|
||||
<li>"What's the status?"</li>
|
||||
<li>"Launch a research agent"</li>
|
||||
<li>"Create a task to find Bitcoin news"</li>
|
||||
<li>"Show me the marketplace"</li>
|
||||
<li>"Emergency stop"</li>
|
||||
</div>
|
||||
|
||||
<div class="voice-tips">
|
||||
<h3>Try saying:</h3>
|
||||
<ul>
|
||||
<li>"What's the status?"</li>
|
||||
<li>"Launch a research agent"</li>
|
||||
<li>"Create a task to find Bitcoin news"</li>
|
||||
<li>"Show me the marketplace"</li>
|
||||
<li>"Emergency stop"</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let recognition = null;
|
||||
let isListening = false;
|
||||
var recognition = null;
|
||||
var isListening = false;
|
||||
|
||||
// Initialize Web Speech API
|
||||
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = false;
|
||||
recognition.lang = 'en-US';
|
||||
|
||||
|
||||
recognition.onstart = function() {
|
||||
isListening = true;
|
||||
document.getElementById('voice-status').textContent = 'Listening...';
|
||||
document.getElementById('voice-btn').classList.add('listening');
|
||||
};
|
||||
|
||||
|
||||
recognition.onresult = function(event) {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
var transcript = event.results[0][0].transcript;
|
||||
processVoiceCommand(transcript);
|
||||
};
|
||||
|
||||
|
||||
recognition.onerror = function(event) {
|
||||
console.error('Speech recognition error:', event.error);
|
||||
document.getElementById('voice-status').textContent = 'Error: ' + event.error;
|
||||
resetButton();
|
||||
};
|
||||
|
||||
|
||||
recognition.onend = function() {
|
||||
isListening = false;
|
||||
resetButton();
|
||||
@@ -141,17 +172,11 @@ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
||||
}
|
||||
|
||||
function startListening() {
|
||||
if (recognition && !isListening) {
|
||||
recognition.start();
|
||||
}
|
||||
if (recognition && !isListening) { recognition.start(); }
|
||||
}
|
||||
|
||||
function stopListening() {
|
||||
if (recognition && isListening) {
|
||||
recognition.stop();
|
||||
}
|
||||
if (recognition && isListening) { recognition.stop(); }
|
||||
}
|
||||
|
||||
function resetButton() {
|
||||
document.getElementById('voice-status').textContent = 'Tap and hold to speak';
|
||||
document.getElementById('voice-btn').classList.remove('listening');
|
||||
@@ -160,33 +185,30 @@ function resetButton() {
|
||||
async function processVoiceCommand(text) {
|
||||
document.getElementById('transcript-text').textContent = text;
|
||||
document.getElementById('voice-status').textContent = 'Processing...';
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/voice/command', {
|
||||
var response = await fetch('/voice/command', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `text=${encodeURIComponent(text)}`
|
||||
body: 'text=' + encodeURIComponent(text)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
var data = await response.json();
|
||||
|
||||
document.getElementById('response-text').textContent = data.command.response;
|
||||
document.getElementById('voice-result').style.display = 'block';
|
||||
document.getElementById('voice-status').textContent = 'Done!';
|
||||
|
||||
// Speak response if supported
|
||||
|
||||
if ('speechSynthesis' in window) {
|
||||
const utterance = new SpeechSynthesisUtterance(data.command.response);
|
||||
var utterance = new SpeechSynthesisUtterance(data.command.response);
|
||||
utterance.rate = 1.1;
|
||||
window.speechSynthesis.speak(utterance);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
document.getElementById('response-text').textContent = 'Sorry, I had trouble processing that.';
|
||||
document.getElementById('voice-result').style.display = 'block';
|
||||
document.getElementById('voice-status').textContent = 'Error';
|
||||
}
|
||||
|
||||
|
||||
setTimeout(resetButton, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,53 +4,117 @@
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.wave-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: 60px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.wave-bar {
|
||||
width: 4px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
animation: wave 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.wave-bar:nth-child(1) { animation-delay: 0s; height: 20%; }
|
||||
.wave-bar:nth-child(2) { animation-delay: 0.1s; height: 40%; }
|
||||
.wave-bar:nth-child(3) { animation-delay: 0.2s; height: 60%; }
|
||||
.wave-bar:nth-child(4) { animation-delay: 0.3s; height: 80%; }
|
||||
.wave-bar:nth-child(5) { animation-delay: 0.4s; height: 100%; }
|
||||
.wave-bar:nth-child(6) { animation-delay: 0.3s; height: 80%; }
|
||||
.wave-bar:nth-child(7) { animation-delay: 0.2s; height: 60%; }
|
||||
.wave-bar:nth-child(8) { animation-delay: 0.1s; height: 40%; }
|
||||
.wave-bar:nth-child(9) { animation-delay: 0s; height: 20%; }
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% { transform: scaleY(0.5); opacity: 0.5; }
|
||||
50% { transform: scaleY(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.wave-container:not(.listening) .wave-bar {
|
||||
animation: none;
|
||||
height: 10%;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.voice-enhanced-page {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wave-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: 60px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.wave-bar {
|
||||
width: 4px;
|
||||
background: var(--purple);
|
||||
border-radius: 2px;
|
||||
animation: wave 1s ease-in-out infinite;
|
||||
}
|
||||
.wave-bar:nth-child(1) { animation-delay: 0s; height: 20%; }
|
||||
.wave-bar:nth-child(2) { animation-delay: 0.1s; height: 40%; }
|
||||
.wave-bar:nth-child(3) { animation-delay: 0.2s; height: 60%; }
|
||||
.wave-bar:nth-child(4) { animation-delay: 0.3s; height: 80%; }
|
||||
.wave-bar:nth-child(5) { animation-delay: 0.4s; height: 100%; }
|
||||
.wave-bar:nth-child(6) { animation-delay: 0.3s; height: 80%; }
|
||||
.wave-bar:nth-child(7) { animation-delay: 0.2s; height: 60%; }
|
||||
.wave-bar:nth-child(8) { animation-delay: 0.1s; height: 40%; }
|
||||
.wave-bar:nth-child(9) { animation-delay: 0s; height: 20%; }
|
||||
@keyframes wave {
|
||||
0%, 100% { transform: scaleY(0.5); opacity: 0.5; }
|
||||
50% { transform: scaleY(1); opacity: 1; }
|
||||
}
|
||||
.wave-container:not(.listening) .wave-bar {
|
||||
animation: none;
|
||||
height: 10%;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.voice-btn-row {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.voice-btn-row button {
|
||||
padding: 12px 32px;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
min-height: 48px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
transition: transform 0.1s, box-shadow 0.2s;
|
||||
}
|
||||
.voice-btn-row button:active { transform: scale(0.96); }
|
||||
#start-btn {
|
||||
background: var(--border-glow);
|
||||
border: none;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
#stop-btn {
|
||||
background: var(--red);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#status-text {
|
||||
text-align: center;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background: rgba(24, 10, 45, 0.8);
|
||||
border: 1px solid var(--border);
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.result-box.timmy-reply {
|
||||
border-left: 3px solid var(--purple);
|
||||
}
|
||||
.result-box strong {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.result-box.timmy-reply strong { color: var(--purple); }
|
||||
|
||||
#audio-player {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card" style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="card-header" style="text-align: center;">
|
||||
<h2 class="card-title">🎙️ Enhanced Voice Control</h2>
|
||||
<p style="color: var(--text-secondary);">Natural language with audio responses</p>
|
||||
</div>
|
||||
|
||||
<div class="wave-container" id="wave-container">
|
||||
<div class="voice-enhanced-page py-3">
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header" style="text-align:center;">// ENHANCED VOICE CONTROL</div>
|
||||
<div class="card-body">
|
||||
<p style="color:var(--text-dim); text-align:center; font-size:0.85rem;">Natural language with audio responses</p>
|
||||
|
||||
<div class="wave-container" id="wave-container">
|
||||
<div class="wave-bar"></div>
|
||||
<div class="wave-bar"></div>
|
||||
<div class="wave-bar"></div>
|
||||
@@ -60,46 +124,41 @@
|
||||
<div class="wave-bar"></div>
|
||||
<div class="wave-bar"></div>
|
||||
<div class="wave-bar"></div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-bottom: 20px;">
|
||||
<button class="btn btn-primary" id="start-btn" onclick="startRecording()" style="padding: 15px 40px; font-size: 1.125rem;">
|
||||
🎤 Start Recording
|
||||
</button>
|
||||
<button class="btn btn-danger" id="stop-btn" onclick="stopRecording()" style="display: none; padding: 15px 40px; font-size: 1.125rem;">
|
||||
⏹ Stop Recording
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="status-text" style="text-align: center; color: var(--text-secondary); margin-bottom: 20px;">
|
||||
Click Start to begin
|
||||
</div>
|
||||
|
||||
<div id="result-container" style="display: none;">
|
||||
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px; margin-bottom: 12px;">
|
||||
<strong style="color: var(--text-muted);">You said:</strong>
|
||||
<p id="user-text" style="margin-top: 8px;"></p>
|
||||
</div>
|
||||
|
||||
<div class="voice-btn-row">
|
||||
<button id="start-btn" onclick="startRecording()">🎤 Start Recording</button>
|
||||
<button id="stop-btn" onclick="stopRecording()" style="display: none;">⏹ Stop Recording</button>
|
||||
</div>
|
||||
|
||||
<div id="status-text">Click Start to begin</div>
|
||||
|
||||
<div id="result-container" style="display: none;">
|
||||
<div class="result-box">
|
||||
<strong>YOU SAID</strong>
|
||||
<p id="user-text" style="margin:0;"></p>
|
||||
</div>
|
||||
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px; border-left: 3px solid var(--accent);">
|
||||
<strong style="color: var(--accent);">Timmy:</strong>
|
||||
<p id="timmy-text" style="margin-top: 8px;"></p>
|
||||
<div class="result-box timmy-reply">
|
||||
<strong>TIMMY</strong>
|
||||
<p id="timmy-text" style="margin:0;"></p>
|
||||
</div>
|
||||
<audio id="audio-player" controls style="width: 100%; margin-top: 12px; display: none;"></audio>
|
||||
<audio id="audio-player" controls style="display: none;"></audio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let recognition = null;
|
||||
let isRecording = false;
|
||||
var recognition = null;
|
||||
var isRecording = false;
|
||||
|
||||
// Initialize speech recognition
|
||||
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = true;
|
||||
recognition.lang = 'en-US';
|
||||
|
||||
|
||||
recognition.onstart = function() {
|
||||
isRecording = true;
|
||||
document.getElementById('start-btn').style.display = 'none';
|
||||
@@ -107,29 +166,25 @@ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
||||
document.getElementById('wave-container').classList.add('listening');
|
||||
document.getElementById('status-text').textContent = 'Listening...';
|
||||
};
|
||||
|
||||
|
||||
recognition.onresult = function(event) {
|
||||
let finalTranscript = '';
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
var finalTranscript = '';
|
||||
for (var i = event.resultIndex; i < event.results.length; i++) {
|
||||
if (event.results[i].isFinal) {
|
||||
finalTranscript += event.results[i][0].transcript;
|
||||
}
|
||||
}
|
||||
if (finalTranscript) {
|
||||
processCommand(finalTranscript);
|
||||
}
|
||||
if (finalTranscript) { processCommand(finalTranscript); }
|
||||
};
|
||||
|
||||
|
||||
recognition.onerror = function(event) {
|
||||
console.error('Speech error:', event.error);
|
||||
document.getElementById('status-text').textContent = 'Error: ' + event.error;
|
||||
stopRecording();
|
||||
};
|
||||
|
||||
|
||||
recognition.onend = function() {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
}
|
||||
if (isRecording) { stopRecording(); }
|
||||
};
|
||||
} else {
|
||||
document.getElementById('status-text').textContent = 'Speech recognition not supported';
|
||||
@@ -137,16 +192,11 @@ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
}
|
||||
if (recognition) { recognition.start(); }
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
isRecording = false;
|
||||
if (recognition) {
|
||||
recognition.stop();
|
||||
}
|
||||
if (recognition) { recognition.stop(); }
|
||||
document.getElementById('start-btn').style.display = 'inline-flex';
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
document.getElementById('wave-container').classList.remove('listening');
|
||||
@@ -155,33 +205,29 @@ function stopRecording() {
|
||||
|
||||
async function processCommand(text) {
|
||||
document.getElementById('user-text').textContent = text;
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/voice/enhanced', {
|
||||
var response = await fetch('/voice/enhanced', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `text=${encodeURIComponent(text)}`
|
||||
body: 'text=' + encodeURIComponent(text)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
var data = await response.json();
|
||||
|
||||
document.getElementById('timmy-text').textContent = data.response_text;
|
||||
document.getElementById('result-container').style.display = 'block';
|
||||
document.getElementById('status-text').textContent = 'Done!';
|
||||
|
||||
// Play audio if available
|
||||
|
||||
if (data.audio_url) {
|
||||
const audio = document.getElementById('audio-player');
|
||||
var audio = document.getElementById('audio-player');
|
||||
audio.src = data.audio_url;
|
||||
audio.style.display = 'block';
|
||||
audio.play();
|
||||
} else {
|
||||
// Fallback to browser TTS
|
||||
const utterance = new SpeechSynthesisUtterance(data.response_text);
|
||||
var utterance = new SpeechSynthesisUtterance(data.response_text);
|
||||
utterance.rate = 1.1;
|
||||
window.speechSynthesis.speak(utterance);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
document.getElementById('timmy-text').textContent = 'Sorry, I had trouble with that.';
|
||||
document.getElementById('result-container').style.display = 'block';
|
||||
|
||||
531
static/style.css
531
static/style.css
@@ -20,6 +20,31 @@
|
||||
--font: 'JetBrains Mono', 'Courier New', monospace;
|
||||
--header-h: 52px;
|
||||
|
||||
/* ── Aliases for templates that use semantic names ── */
|
||||
--bg-primary: var(--bg-deep);
|
||||
--bg-secondary: var(--bg-panel);
|
||||
--bg-tertiary: var(--bg-card);
|
||||
--text-primary: var(--text-bright);
|
||||
--text-secondary: var(--text);
|
||||
--text-muted: var(--text-dim);
|
||||
--accent: var(--orange);
|
||||
--accent-dim: #b35500;
|
||||
--success: var(--green);
|
||||
--warning: var(--amber);
|
||||
--danger: var(--red);
|
||||
--info: var(--purple);
|
||||
|
||||
/* ── Radius & spacing tokens ── */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 14px;
|
||||
--radius-xl: 20px;
|
||||
|
||||
/* ── Glass effect ── */
|
||||
--glass-bg: rgba(17, 8, 32, 0.72);
|
||||
--glass-blur: 16px;
|
||||
--glass-border: rgba(124, 58, 237, 0.18);
|
||||
|
||||
/* Bootstrap dark-mode overrides */
|
||||
--bs-body-bg: var(--bg-deep);
|
||||
--bs-body-color: var(--text);
|
||||
@@ -34,19 +59,38 @@
|
||||
--bs-form-control-color: var(--text-bright);
|
||||
}
|
||||
|
||||
/* ── Reset & base ─────────────────────────────────── */
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font);
|
||||
background-color: var(--bg-deep);
|
||||
background-image: url('/static/bg.svg');
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-attachment: fixed;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
min-height: 100dvh;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a { color: var(--purple); }
|
||||
a:hover { color: var(--orange); }
|
||||
|
||||
/* Smooth page transitions */
|
||||
.mc-main { animation: fadeUp 0.3s ease-out; }
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Header ─────────────────────────────────────── */
|
||||
@@ -72,7 +116,9 @@ body {
|
||||
color: var(--text-bright);
|
||||
letter-spacing: 0.15em;
|
||||
text-shadow: 0 0 18px rgba(168, 85, 247, 0.55), 0 0 40px rgba(168, 85, 247, 0.25);
|
||||
text-decoration: none;
|
||||
}
|
||||
.mc-title:hover { color: var(--text-bright); text-decoration: none; }
|
||||
.mc-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
@@ -99,6 +145,108 @@ body {
|
||||
}
|
||||
.mc-test-link:hover { border-color: var(--purple); color: var(--purple); }
|
||||
|
||||
/* ── Hamburger (mobile only) ───────────────────── */
|
||||
.mc-hamburger {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.mc-hamburger span {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 2px;
|
||||
background: var(--text);
|
||||
border-radius: 1px;
|
||||
transition: transform 0.3s ease, opacity 0.2s ease;
|
||||
}
|
||||
.mc-hamburger.open span:nth-child(1) { transform: rotate(45deg) translate(5px, 5px); }
|
||||
.mc-hamburger.open span:nth-child(2) { opacity: 0; }
|
||||
.mc-hamburger.open span:nth-child(3) { transform: rotate(-45deg) translate(5px, -5px); }
|
||||
|
||||
/* ── Mobile slide-out menu ─────────────────────── */
|
||||
.mc-mobile-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 199;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.mc-mobile-overlay.open { display: block; opacity: 1; }
|
||||
|
||||
.mc-mobile-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -280px;
|
||||
width: 280px;
|
||||
height: 100%;
|
||||
background: rgba(17, 8, 32, 0.96);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-left: 1px solid var(--border);
|
||||
z-index: 200;
|
||||
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: max(12px, env(safe-area-inset-top));
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.mc-mobile-menu.open { right: 0; }
|
||||
|
||||
.mc-mobile-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.mc-mobile-menu-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
.mc-mobile-link {
|
||||
display: block;
|
||||
padding: 14px 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.12em;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid rgba(59, 26, 92, 0.3);
|
||||
transition: background 0.15s, color 0.15s, padding-left 0.2s;
|
||||
}
|
||||
.mc-mobile-link:hover,
|
||||
.mc-mobile-link.active {
|
||||
background: rgba(124, 58, 237, 0.1);
|
||||
color: var(--text-bright);
|
||||
padding-left: 28px;
|
||||
}
|
||||
.mc-mobile-link.active {
|
||||
border-left: 3px solid var(--orange);
|
||||
color: var(--orange);
|
||||
}
|
||||
.mc-mobile-menu-footer {
|
||||
margin-top: auto;
|
||||
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ── Main layout ─────────────────────────────────── */
|
||||
.mc-main {
|
||||
padding: 16px;
|
||||
@@ -129,11 +277,15 @@ body {
|
||||
|
||||
/* ── Panel / Card overrides ──────────────────────── */
|
||||
.mc-panel {
|
||||
background: rgba(17, 8, 32, 0.78);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
.mc-panel:hover {
|
||||
border-color: rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
.mc-panel-header {
|
||||
background: rgba(24, 10, 45, 0.90);
|
||||
@@ -149,9 +301,13 @@ body {
|
||||
/* ── Agent Card ──────────────────────────────────── */
|
||||
.mc-agent-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px;
|
||||
background: rgba(24, 10, 45, 0.82);
|
||||
transition: border-color 0.2s, transform 0.15s;
|
||||
}
|
||||
.mc-agent-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
@@ -174,6 +330,45 @@ body {
|
||||
.meta-key { color: var(--text-dim); display: inline-block; width: 60px; }
|
||||
.meta-val { color: var(--text); }
|
||||
|
||||
/* ── Generic agent card (used by mobile, marketplace, swarm) ── */
|
||||
.agent-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
margin-bottom: 10px;
|
||||
transition: border-color 0.2s, transform 0.15s;
|
||||
}
|
||||
.agent-card:active { transform: scale(0.98); }
|
||||
.agent-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, var(--purple), var(--orange));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agent-info { flex: 1; min-width: 0; }
|
||||
.agent-info .agent-name {
|
||||
font-size: 13px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.agent-info .agent-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Health ──────────────────────────────────────── */
|
||||
.health-row {
|
||||
display: flex;
|
||||
@@ -191,6 +386,13 @@ body {
|
||||
.mc-badge-down { background: var(--red-dim) !important; color: var(--red) !important; font-size: 10px; letter-spacing: 0.12em; border-radius: 2px; }
|
||||
.mc-badge-ready { background: var(--amber-dim) !important; color: var(--amber) !important; font-size: 10px; letter-spacing: 0.12em; border-radius: 2px; }
|
||||
|
||||
/* Generic badges (used across pages) */
|
||||
.badge-success { background: var(--green-dim) !important; color: var(--green) !important; }
|
||||
.badge-warning { background: var(--amber-dim) !important; color: var(--amber) !important; }
|
||||
.badge-danger { background: var(--red-dim) !important; color: var(--red) !important; }
|
||||
.badge-secondary { background: rgba(59, 26, 92, 0.5) !important; color: var(--text-dim) !important; }
|
||||
.badge-info { background: rgba(168, 85, 247, 0.2) !important; color: var(--purple) !important; }
|
||||
|
||||
/* ── Chat ────────────────────────────────────────── */
|
||||
.chat-log {
|
||||
flex: 1;
|
||||
@@ -211,7 +413,7 @@ body {
|
||||
.msg-body {
|
||||
background: rgba(24, 10, 45, 0.80);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 12px;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
@@ -221,6 +423,25 @@ body {
|
||||
.chat-message.agent .msg-body { border-left: 3px solid var(--purple); }
|
||||
.chat-message.error-msg .msg-body { border-left: 3px solid var(--red); color: var(--red); }
|
||||
|
||||
/* Mobile chat classes (used by mobile.html) */
|
||||
.chat-container {
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 12px;
|
||||
}
|
||||
.chat-message .chat-meta {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.chat-message.user .chat-meta { color: var(--orange); }
|
||||
.chat-message.timmy .chat-meta { color: var(--purple); }
|
||||
.chat-message.timmy > div:last-child {
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Chat input footer ───────────────────────────── */
|
||||
.mc-chat-footer {
|
||||
padding: 12px 14px;
|
||||
@@ -264,14 +485,14 @@ body {
|
||||
.mc-btn-send {
|
||||
background: var(--border-glow);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-bright);
|
||||
font-family: var(--font);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 8px 18px;
|
||||
letter-spacing: 0.12em;
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s, transform 0.1s;
|
||||
touch-action: manipulation;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -280,6 +501,135 @@ body {
|
||||
color: #080412;
|
||||
box-shadow: 0 0 14px rgba(249, 115, 22, 0.45);
|
||||
}
|
||||
.mc-btn-send:active { transform: scale(0.96); }
|
||||
|
||||
/* ── Buttons (generic) ──────────────────────────── */
|
||||
.btn-primary {
|
||||
background: var(--border-glow) !important;
|
||||
border-color: var(--border-glow) !important;
|
||||
color: var(--text-bright) !important;
|
||||
font-family: var(--font);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
transition: background 0.15s, box-shadow 0.2s, transform 0.1s;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--purple) !important;
|
||||
box-shadow: 0 0 16px rgba(168, 85, 247, 0.4);
|
||||
}
|
||||
.btn-primary:active { transform: scale(0.97); }
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(59, 26, 92, 0.3) !important;
|
||||
border-color: var(--border) !important;
|
||||
color: var(--text) !important;
|
||||
font-family: var(--font);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: rgba(59, 26, 92, 0.5) !important;
|
||||
border-color: var(--purple) !important;
|
||||
}
|
||||
|
||||
/* ── Grid utilities (used across pages) ────────── */
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
|
||||
/* ── Stat cards ─────────────────────────────────── */
|
||||
.stat {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-bright);
|
||||
font-family: var(--font);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ── Form elements ──────────────────────────────── */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
background: rgba(8, 4, 18, 0.75);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-bright);
|
||||
font-family: var(--font);
|
||||
font-size: 13px;
|
||||
padding: 10px 12px;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-glow);
|
||||
box-shadow: 0 0 0 1px var(--border-glow), 0 0 10px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
.form-group input::placeholder,
|
||||
.form-group textarea::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.form-group small {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ── Card enhancements (Bootstrap cards in theme) ── */
|
||||
.card {
|
||||
background: var(--glass-bg) !important;
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
border: 1px solid var(--glass-border) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-header {
|
||||
background: rgba(24, 10, 45, 0.90) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.card-title {
|
||||
color: var(--text-bright);
|
||||
font-family: var(--font);
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* ── HTMX Loading ────────────────────────────────── */
|
||||
.htmx-indicator { display: none; }
|
||||
@@ -293,52 +643,187 @@ body {
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--border-glow); }
|
||||
|
||||
/* ── Nav tabs (Bootstrap override) ───────────────── */
|
||||
.nav-tabs {
|
||||
border-bottom-color: var(--border) !important;
|
||||
}
|
||||
.nav-tabs .nav-link {
|
||||
color: var(--text-dim) !important;
|
||||
font-family: var(--font);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
border-color: transparent !important;
|
||||
padding: 8px 16px;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: var(--text) !important;
|
||||
border-color: transparent transparent var(--border) !important;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
color: var(--orange) !important;
|
||||
background: transparent !important;
|
||||
border-color: var(--border) var(--border) var(--bg-deep) !important;
|
||||
}
|
||||
|
||||
|
||||
/* ════════════════════════════════════════════════════
|
||||
MOBILE (≤ 768 px)
|
||||
MOBILE (max-width: 768px) — iPhone optimized
|
||||
════════════════════════════════════════════════════ */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
:root { --header-h: 44px; }
|
||||
:root { --header-h: 48px; }
|
||||
|
||||
/* Compact header */
|
||||
.mc-header { padding: 10px 16px; padding-top: max(10px, env(safe-area-inset-top)); }
|
||||
/* ── Header: hide desktop nav, show hamburger ── */
|
||||
.mc-header {
|
||||
padding: 0 12px;
|
||||
padding-top: max(0px, env(safe-area-inset-top));
|
||||
height: var(--header-h);
|
||||
gap: 8px;
|
||||
}
|
||||
.mc-desktop-nav { display: none !important; }
|
||||
.mc-hamburger { display: flex; }
|
||||
.mc-title { font-size: 14px; letter-spacing: 0.1em; }
|
||||
.mc-subtitle { display: none; }
|
||||
.mc-time { font-size: 12px; }
|
||||
|
||||
/* Full-height layout becomes scrollable column stack */
|
||||
.mc-main { height: auto; overflow: visible; padding: 8px; }
|
||||
/* ── Main layout becomes scrollable column stack ── */
|
||||
.mc-main {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
padding: 10px;
|
||||
padding-bottom: max(10px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.mc-content, .mc-content > .row { height: auto; }
|
||||
|
||||
/* Sidebar becomes a horizontal scroll strip */
|
||||
/* ── Sidebar becomes a horizontal scroll strip ── */
|
||||
.mc-sidebar {
|
||||
flex-direction: row !important;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.mc-sidebar::-webkit-scrollbar { display: none; }
|
||||
.mc-sidebar .mc-panel {
|
||||
min-width: 200px;
|
||||
min-width: 220px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Chat fills remaining space with defined height */
|
||||
/* ── Chat fills remaining space ── */
|
||||
.mc-chat-panel { min-height: 60dvh; }
|
||||
.mc-chat-panel > .card { height: 60dvh; }
|
||||
|
||||
/* Tighter message padding */
|
||||
/* ── Tighter chat ── */
|
||||
.chat-log { padding: 10px !important; }
|
||||
.msg-body { padding: 8px 10px; font-size: 13px; }
|
||||
.chat-message { margin-bottom: 12px; }
|
||||
|
||||
/* Touch-friendly input */
|
||||
/* ── Touch-friendly inputs ── */
|
||||
.mc-chat-footer {
|
||||
padding: 8px 10px;
|
||||
padding-bottom: max(8px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.mc-input { font-size: 16px !important; min-height: 44px; }
|
||||
.mc-btn-send { min-height: 44px; min-width: 64px; font-size: 12px; padding: 0 14px; }
|
||||
.mc-input {
|
||||
font-size: 16px !important;
|
||||
min-height: 44px;
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
.mc-btn-send {
|
||||
min-height: 44px;
|
||||
min-width: 64px;
|
||||
font-size: 12px;
|
||||
padding: 0 14px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.mc-btn-clear {
|
||||
min-height: 36px;
|
||||
padding: 6px 10px;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ── Agent cards ── */
|
||||
.mc-agent-card { padding: 10px; }
|
||||
|
||||
/* ── Grids collapse ── */
|
||||
.grid-2, .grid-3 { grid-template-columns: 1fr; }
|
||||
.stat-value { font-size: 1.3rem; }
|
||||
|
||||
/* ── Bootstrap columns stack ── */
|
||||
.col-md-3, .col-md-4, .col-md-6, .col-md-9 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.col-lg-4 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* ── Form elements ── */
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
font-size: 16px;
|
||||
min-height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* ── Cards ── */
|
||||
.card {
|
||||
border-radius: var(--radius-md) !important;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-header {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ── Tab nav scrollable ── */
|
||||
.nav-tabs {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.nav-tabs::-webkit-scrollbar { display: none; }
|
||||
.nav-tabs .nav-link {
|
||||
white-space: nowrap;
|
||||
padding: 8px 14px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* ── touch-button (used by mobile.html) ── */
|
||||
.touch-button {
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: transform 0.1s, box-shadow 0.2s;
|
||||
}
|
||||
.touch-button:active { transform: scale(0.96); }
|
||||
|
||||
/* ── Display utilities ── */
|
||||
.display-6 { font-size: 1.2rem !important; }
|
||||
|
||||
/* ── Container-fluid tight ── */
|
||||
.container-fluid { padding-left: 10px; padding-right: 10px; }
|
||||
}
|
||||
|
||||
|
||||
/* ════════════════════════════════════════════════════
|
||||
SMALL MOBILE (max-width: 390px) — iPhone SE / mini
|
||||
════════════════════════════════════════════════════ */
|
||||
@media (max-width: 390px) {
|
||||
.mc-title { font-size: 12px; }
|
||||
.mc-sidebar .mc-panel { min-width: 180px; }
|
||||
.mc-btn-send { min-width: 54px; padding: 0 10px; }
|
||||
.stat-value { font-size: 1.1rem; }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class TestSwarmLivePage:
|
||||
|
||||
def test_swarm_live_contains_dashboard_title(self, client):
|
||||
resp = client.get("/swarm/live")
|
||||
assert "Live Swarm Dashboard" in resp.text
|
||||
assert "LIVE SWARM" in resp.text
|
||||
|
||||
def test_swarm_live_contains_websocket_script(self, client):
|
||||
resp = client.get("/swarm/live")
|
||||
|
||||
Reference in New Issue
Block a user