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:
Claude
2026-02-24 22:25:04 +00:00
parent d96b7593fc
commit 65a278dbee
13 changed files with 2121 additions and 1318 deletions

View File

@@ -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">&#x1F514;</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;">&#x1F514; 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>

View File

@@ -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&rsquo;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&hellip;</div>
<div style="text-align:center; color:var(--text-dim); padding:16px; font-size:0.85rem;">Loading approval items&hellip;</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 %}

View File

@@ -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 &amp; 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 %}

View File

@@ -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 &mdash; 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> &mdash; 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> &mdash; 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> &mdash; 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&amp;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> &mdash; 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&amp;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> &mdash; 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> &mdash; 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> &mdash; 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> &rarr; <span class="badge bg-primary">Storyboard</span> &rarr; <span class="badge bg-success">Music</span> &rarr; <span class="badge bg-warning text-dark">Video</span> &rarr; <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> &mdash; 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> &rarr;
<span class="badge badge-info">Storyboard</span> &rarr;
<span class="badge badge-success">Music</span> &rarr;
<span class="badge badge-warning">Video</span> &rarr;
<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 %}

View File

@@ -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
&nbsp;&middot;&nbsp;
<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 &middot;
<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 &mdash; 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 &mdash; 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 %}

View File

@@ -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">&#x1F3A4; Voice</a>
<a href="/swarm/task/create" class="quick-btn">&#x2795; Task</a>
<a href="/swarm/live" class="quick-btn">&#x1F4CA; Swarm</a>
<a href="/marketplace/ui" class="quick-btn">&#x1F3EA; 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;">&#x1F4F1;</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;
}

View File

@@ -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 &mdash;
@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 &amp; 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 %}

View File

@@ -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()">
&#x1F3A4;
</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>

View File

@@ -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()">&#x1F3A4; Start Recording</button>
<button id="stop-btn" onclick="stopRecording()" style="display: none;">&#x23F9; 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';

View File

@@ -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; }
}

View File

@@ -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")