Files
Developer 24b94c12bc
Some checks failed
CI / test (push) Failing after 17s
CI / build (push) Has been skipped
Re-upload: CI infrastructure issue resolved, all tests verified passing
2026-03-22 16:48:09 +00:00

1536 lines
55 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>7000%AUTO | AI Automation Dashboard</title>
<style>
:root {
--bg-white: #ffffff;
--bg-light: #f8f9fa;
--bg-card: #ffffff;
--bg-card-hover: #f0f4f8;
--border: #e1e5eb;
--border-light: #f0f2f5;
--text-primary: #1a1a2e;
--text-secondary: #5a6474;
--text-muted: #9ca3af;
/* Brand Colors */
--accent-cyan: #1BC6F5;
--accent-lime: #B1EB4E;
--accent-purple: #D17DE2;
/* Derived Colors */
--accent-cyan-light: rgba(27, 198, 245, 0.15);
--accent-lime-light: rgba(177, 235, 78, 0.15);
--accent-purple-light: rgba(209, 125, 226, 0.15);
--glow-cyan: rgba(27, 198, 245, 0.4);
--glow-lime: rgba(177, 235, 78, 0.4);
--glow-purple: rgba(209, 125, 226, 0.4);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
background: var(--bg-white);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.5;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-light);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 16px;
}
.logo-img {
height: 40px;
width: auto;
}
.logo-text {
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-badge {
font-size: 12px;
padding: 4px 10px;
background: var(--accent-lime-light);
border: 1px solid var(--accent-lime);
border-radius: 20px;
color: #5a8c1a;
font-weight: 500;
}
.status-group {
display: flex;
gap: 24px;
align-items: center;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--text-secondary);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
}
.status-dot.running {
background: var(--accent-lime);
box-shadow: 0 0 12px var(--glow-lime);
animation: pulse 2s infinite;
}
.status-dot.idle {
background: var(--accent-cyan);
}
.status-dot.error {
background: #ef4444;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Grid Layout */
.grid {
display: grid;
grid-template-columns: 1fr 380px;
gap: 24px;
}
.main-content {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Cards */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.card:hover {
box-shadow: var(--shadow-md);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-light);
}
.card-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.card-title-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.card-title-icon svg {
width: 18px;
height: 18px;
}
.card-badge {
font-size: 12px;
padding: 4px 12px;
border-radius: 20px;
background: var(--bg-light);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.card-badge.active {
background: var(--accent-lime-light);
color: #5a8c1a;
border-color: var(--accent-lime);
}
.card-body {
padding: 20px;
}
/* Current Project */
.project-info {
text-align: center;
padding: 20px 0;
}
.project-info.has-project {
text-align: left;
}
.project-name {
font-size: 20px;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-primary);
}
.project-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
text-transform: capitalize;
}
.project-status-badge.ideation { background: var(--accent-purple-light); color: #9b4dca; }
.project-status-badge.planning { background: var(--accent-cyan-light); color: #0891b2; }
.project-status-badge.development { background: var(--accent-lime-light); color: #5a8c1a; }
.project-status-badge.testing { background: rgba(251, 191, 36, 0.15); color: #b45309; }
.project-status-badge.uploading { background: var(--accent-cyan-light); color: #0891b2; }
.project-status-badge.promoting { background: var(--accent-purple-light); color: #9b4dca; }
.project-status-badge.completed { background: var(--accent-lime-light); color: #5a8c1a; }
.project-meta {
margin-top: 12px;
font-size: 13px;
color: var(--text-secondary);
}
.no-project {
color: var(--text-muted);
font-size: 14px;
}
.no-project-icon {
font-size: 32px;
margin-bottom: 12px;
opacity: 0.5;
}
/* Agent Pipeline */
.pipeline {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 16px;
position: relative;
}
.pipeline::before {
content: '';
position: absolute;
top: 50%;
left: 64px;
right: 64px;
height: 3px;
background: var(--border);
transform: translateY(-50%);
z-index: 0;
border-radius: 2px;
}
.pipeline-progress {
position: absolute;
top: 50%;
left: 64px;
height: 3px;
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-lime));
transform: translateY(-50%);
z-index: 1;
transition: width 0.5s ease;
border-radius: 2px;
box-shadow: 0 0 10px var(--glow-cyan);
}
.agent-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
position: relative;
z-index: 2;
}
.agent-icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--bg-white);
border: 3px solid var(--border);
transition: all 0.3s ease;
padding: 14px;
box-shadow: var(--shadow-sm);
}
.agent-icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
.agent-node.completed .agent-icon {
border-color: var(--accent-lime);
background: var(--accent-lime-light);
box-shadow: 0 0 15px var(--glow-lime);
}
.agent-node.active .agent-icon {
border-color: var(--accent-cyan);
background: var(--accent-cyan-light);
box-shadow: 0 0 20px var(--glow-cyan);
animation: agent-active 1.5s infinite;
}
.agent-node.pending .agent-icon {
opacity: 0.4;
}
@keyframes agent-active {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
.agent-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
}
.agent-node.completed .agent-label { color: #5a8c1a; }
.agent-node.active .agent-label { color: var(--accent-cyan); font-weight: 700; }
/* Iteration Counter */
.iteration-box {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 16px;
background: linear-gradient(135deg, var(--accent-cyan-light), var(--accent-purple-light));
border-radius: 12px;
margin-top: 16px;
border: 1px solid var(--border);
}
.iteration-label {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.iteration-value {
font-size: 24px;
font-weight: 700;
color: var(--accent-purple);
font-variant-numeric: tabular-nums;
}
/* Current Agent Output - Main Display */
.agent-output-main {
flex: 1;
display: flex;
flex-direction: column;
min-height: 400px;
}
.agent-output-display {
flex: 1;
padding: 24px;
background: var(--bg-light);
border-radius: 16px;
overflow-y: auto;
max-height: 500px;
border: 1px solid var(--border-light);
}
.agent-output-display-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 2px solid var(--border);
}
.agent-output-display-icon {
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--accent-cyan-light);
border: 3px solid var(--accent-cyan);
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
box-shadow: 0 0 20px var(--glow-cyan);
animation: agent-active 1.5s infinite;
}
.agent-output-display-icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
.agent-output-display-info {
flex: 1;
}
.agent-output-display-name {
font-size: 24px;
font-weight: 700;
color: var(--accent-cyan);
text-transform: capitalize;
}
.agent-output-display-status {
font-size: 14px;
color: var(--text-secondary);
margin-top: 4px;
}
.agent-output-display-status.running {
color: var(--accent-lime);
}
.agent-output-display-content {
font-size: 14px;
line-height: 1.7;
color: var(--text-primary);
white-space: pre-wrap;
word-break: break-word;
min-height: 200px;
max-height: 350px;
overflow-y: auto;
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
padding: 16px;
background: var(--bg-white);
border-radius: 8px;
border: 1px solid var(--border-light);
}
.agent-output-display-content.streaming::after {
content: '▋';
animation: cursor-blink 1s infinite;
color: var(--accent-cyan);
}
@keyframes cursor-blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.agent-output-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 300px;
color: var(--text-muted);
}
.agent-output-empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.agent-output-empty-text {
font-size: 18px;
}
/* Sidebar */
.sidebar {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Stats */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.stat-item {
background: var(--bg-light);
padding: 16px;
border-radius: 12px;
text-align: center;
border: 1px solid var(--border-light);
transition: all 0.2s ease;
}
.stat-item:hover {
border-color: var(--accent-cyan);
box-shadow: 0 0 10px var(--glow-cyan);
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: var(--accent-lime);
font-variant-numeric: tabular-nums;
}
.stat-value.cyan { color: var(--accent-cyan); }
.stat-value.purple { color: var(--accent-purple); }
.stat-value.lime { color: #5a8c1a; }
.stat-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 4px;
font-weight: 500;
}
/* Activity Log (sidebar) */
.activity-log {
background: var(--bg-light);
border-radius: 12px;
padding: 12px;
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-light);
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px;
border-radius: 8px;
margin-bottom: 6px;
font-size: 12px;
animation: activity-slide 0.3s ease;
}
.activity-item:last-child {
margin-bottom: 0;
}
.activity-item:hover {
background: var(--bg-card-hover);
}
@keyframes activity-slide {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.activity-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-top: 4px;
flex-shrink: 0;
}
.activity-dot.info { background: var(--accent-cyan); }
.activity-dot.output { background: var(--accent-lime); }
.activity-dot.error { background: #ef4444; }
.activity-content {
flex: 1;
min-width: 0;
}
.activity-agent {
font-weight: 600;
color: var(--accent-purple);
text-transform: capitalize;
}
.activity-message {
color: var(--text-secondary);
margin-top: 2px;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.activity-time {
font-size: 10px;
color: var(--text-muted);
flex-shrink: 0;
}
.activity-empty {
text-align: center;
color: var(--text-muted);
padding: 20px 0;
font-size: 13px;
}
/* Responsive */
@media (max-width: 1024px) {
.grid {
grid-template-columns: 1fr;
}
.pipeline {
flex-wrap: wrap;
gap: 16px;
justify-content: center;
}
.pipeline::before,
.pipeline-progress {
display: none;
}
}
@media (max-width: 600px) {
.container {
padding: 16px;
}
.header {
flex-direction: column;
gap: 16px;
}
.status-group {
flex-wrap: wrap;
justify-content: center;
}
.stats-grid {
grid-template-columns: 1fr;
}
.agent-icon {
width: 56px;
height: 56px;
padding: 10px;
}
.agent-output-display-icon {
width: 40px;
height: 40px;
}
.agent-output-display-name {
font-size: 18px;
}
}
/* Connection Status */
.connection-status {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 20px;
font-size: 12px;
color: var(--text-secondary);
z-index: 1000;
box-shadow: var(--shadow-md);
}
.connection-status.connected {
border-color: var(--accent-lime);
}
.connection-status.disconnected {
border-color: #ef4444;
}
.connection-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
}
.connection-status.connected .connection-dot {
background: var(--accent-lime);
animation: pulse 2s infinite;
}
.connection-status.disconnected .connection-dot {
background: #ef4444;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="logo">
<img src="/dashboard/images/Logo.svg" alt="7000%AUTO Logo" class="logo-img" onerror="this.style.display='none'">
<span class="logo-text">7000%AUTO</span>
<span class="logo-badge">Dashboard v1.0</span>
</div>
<div class="status-group">
<div class="status-item">
<div class="status-dot" id="orchestrator-dot"></div>
<span id="orchestrator-status">Connecting...</span>
</div>
<div class="status-item">
<span id="last-update">--</span>
</div>
</div>
</header>
<!-- Main Grid -->
<div class="grid">
<main class="main-content">
<!-- Current Project -->
<div class="card">
<div class="card-header">
<span class="card-title">
<span class="card-title-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</span>
Current Project
</span>
<span class="card-badge" id="project-badge">No Project</span>
</div>
<div class="card-body">
<div class="project-info" id="project-info">
<div class="no-project">
<div class="no-project-icon"></div>
<div>Waiting for a project to start...</div>
</div>
</div>
</div>
</div>
<!-- Agent Pipeline -->
<div class="card">
<div class="card-header">
<span class="card-title">
<span class="card-title-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</span>
Agent Pipeline
</span>
<span class="card-badge" id="pipeline-stage">Idle</span>
</div>
<div class="card-body">
<div class="pipeline" id="pipeline">
<div class="pipeline-progress" id="pipeline-progress"></div>
<div class="agent-node pending" data-agent="ideator">
<div class="agent-icon">
<img src="/dashboard/images/Ideator.svg" alt="Ideator" onerror="this.parentElement.innerHTML='💡'">
</div>
<span class="agent-label">Ideator</span>
</div>
<div class="agent-node pending" data-agent="planner">
<div class="agent-icon">
<img src="/dashboard/images/Planner.svg" alt="Planner" onerror="this.parentElement.innerHTML='📋'">
</div>
<span class="agent-label">Planner</span>
</div>
<div class="agent-node pending" data-agent="developer">
<div class="agent-icon">
<img src="/dashboard/images/Developer.svg" alt="Developer" onerror="this.parentElement.innerHTML='👨‍💻'">
</div>
<span class="agent-label">Developer</span>
</div>
<div class="agent-node pending" data-agent="tester">
<div class="agent-icon">
<img src="/dashboard/images/Tester.svg" alt="Tester" onerror="this.parentElement.innerHTML='🧪'">
</div>
<span class="agent-label">Tester</span>
</div>
<div class="agent-node pending" data-agent="uploader">
<div class="agent-icon">
<img src="/dashboard/images/Uploader.svg" alt="Uploader" onerror="this.parentElement.innerHTML='🚀'">
</div>
<span class="agent-label">Uploader</span>
</div>
<div class="agent-node pending" data-agent="evangelist">
<div class="agent-icon">
<img src="/dashboard/images/Evangelist.svg" alt="Evangelist" onerror="this.parentElement.innerHTML='📣'">
</div>
<span class="agent-label">Evangelist</span>
</div>
</div>
<div class="iteration-box">
<span class="iteration-label">Developer ⇄ Tester Iterations</span>
<span class="iteration-value" id="iteration-count">0</span>
</div>
</div>
</div>
<!-- Current Agent Output -->
<div class="card agent-output-main">
<div class="card-header">
<span class="card-title">
<span class="card-title-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</span>
Current Agent Output
</span>
<span class="card-badge" id="output-status">Waiting</span>
</div>
<div class="card-body" style="flex: 1; display: flex; flex-direction: column;">
<div class="agent-output-display" id="agent-output-main">
<div class="agent-output-empty">
<div class="agent-output-empty-icon">🤖</div>
<div class="agent-output-empty-text">Waiting for agent activity...</div>
</div>
</div>
</div>
</div>
</main>
<!-- Sidebar -->
<aside class="sidebar">
<!-- Stats -->
<div class="card">
<div class="card-header">
<span class="card-title">
<span class="card-title-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</span>
Statistics
</span>
</div>
<div class="card-body">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" id="stat-total">0</div>
<div class="stat-label">Total Projects</div>
</div>
<div class="stat-item">
<div class="stat-value cyan" id="stat-completed">0</div>
<div class="stat-label">Completed</div>
</div>
<div class="stat-item">
<div class="stat-value purple" id="stat-ideas">0</div>
<div class="stat-label">Ideas</div>
</div>
<div class="stat-item">
<div class="stat-value lime" id="stat-inprogress">0</div>
<div class="stat-label">In Progress</div>
</div>
</div>
</div>
</div>
<!-- Activity Log (compact) -->
<div class="card">
<div class="card-header">
<span class="card-title">
<span class="card-title-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</span>
Activity Log
</span>
</div>
<div class="card-body">
<div class="activity-log" id="activity-log">
<div class="activity-empty">No activity yet</div>
</div>
</div>
</div>
</aside>
</div>
</div>
<!-- Connection Status Indicator -->
<div class="connection-status" id="connection-status">
<div class="connection-dot"></div>
<span id="connection-text">Connecting...</span>
</div>
<script>
const Dashboard = {
// State
activityItems: [],
maxActivityItems: 20,
eventSource: null,
reconnectAttempts: 0,
maxReconnectAttempts: 10,
agents: ['ideator', 'planner', 'developer', 'tester', 'uploader', 'evangelist'],
lastKnownAgent: null, // Track last known agent to prevent unwanted resets
currentAgentOutput: '', // Current agent's streaming output
currentAgent: null, // Currently active agent
isStreaming: false, // Whether we're currently streaming output
statusToAgent: {
'ideation': 'ideator',
'planning': 'planner',
'development': 'developer',
'testing': 'tester',
'uploading': 'uploader',
'promoting': 'evangelist'
},
agentIcons: {
'ideator': '/dashboard/images/Ideator.svg',
'planner': '/dashboard/images/Planner.svg',
'developer': '/dashboard/images/Developer.svg',
'tester': '/dashboard/images/Tester.svg',
'uploader': '/dashboard/images/Uploader.svg',
'evangelist': '/dashboard/images/Evangelist.svg',
'SYSTEM': null
},
fallbackEmojis: {
'ideator': '💡', 'planner': '📋', 'developer': '👨‍💻',
'tester': '🧪', 'uploader': '🚀', 'evangelist': '📣'
},
init() {
console.log('🚀 Dashboard initializing...');
this.connectSSE();
this.fetchInitialData();
},
// Get base path for API calls (handles /dashboard mount)
getBasePath() {
const path = window.location.pathname;
// If we're at /dashboard or /dashboard/, use that as base
if (path.startsWith('/dashboard')) {
return '/dashboard';
}
return '';
},
async fetchInitialData() {
try {
const response = await fetch(this.getBasePath() + '/api/status');
const data = await response.json();
this.updateFromStatus(data);
} catch (e) {
console.error('Failed to fetch initial data:', e);
}
},
connectSSE() {
if (this.eventSource) {
this.eventSource.close();
}
console.log('📡 Connecting to SSE stream...');
this.updateConnectionStatus('connecting');
this.eventSource = new EventSource(this.getBasePath() + '/api/stream');
this.eventSource.onopen = () => {
console.log('✅ SSE connected');
this.reconnectAttempts = 0;
this.updateConnectionStatus('connected');
};
this.eventSource.addEventListener('status', (e) => {
const data = JSON.parse(e.data);
this.updateOrchestratorStatus(data);
});
this.eventSource.addEventListener('init', (e) => {
const data = JSON.parse(e.data);
this.updateFromStatus(data);
});
// Separate event handlers for cleaner data flow
this.eventSource.addEventListener('status_update', (e) => {
const data = JSON.parse(e.data);
this.handleStatusUpdate(data);
});
this.eventSource.addEventListener('project_update', (e) => {
const data = JSON.parse(e.data);
this.updateProject(data);
});
this.eventSource.addEventListener('agent_update', (e) => {
const data = JSON.parse(e.data);
if (data.agent) {
this.updatePipeline(data.agent);
}
});
this.eventSource.addEventListener('iteration_update', (e) => {
const data = JSON.parse(e.data);
if (data.iterations !== undefined) {
this.updateIterationCount(data.iterations);
}
});
this.eventSource.addEventListener('heartbeat', (e) => {
// Just update the timestamp - heartbeat is now a simple ping
this.updateLastUpdate();
});
this.eventSource.addEventListener('log', (e) => {
const data = JSON.parse(e.data);
this.addActivityItem(data);
});
this.eventSource.addEventListener('agent_started', (e) => {
const data = JSON.parse(e.data);
this.handleAgentStarted(data);
});
this.eventSource.addEventListener('agent_completed', (e) => {
const data = JSON.parse(e.data);
this.handleAgentCompleted(data);
});
this.eventSource.addEventListener('agent_error', (e) => {
const data = JSON.parse(e.data);
this.handleAgentError(data);
});
this.eventSource.addEventListener('workflow_started', (e) => {
const data = JSON.parse(e.data);
this.handleWorkflowStarted(data);
});
this.eventSource.addEventListener('workflow_completed', (e) => {
const data = JSON.parse(e.data);
this.handleWorkflowCompleted(data);
});
this.eventSource.addEventListener('iteration_started', (e) => {
const data = JSON.parse(e.data);
this.updateIterationCount(data.data?.iteration || 0);
});
this.eventSource.addEventListener('agent_output', (e) => {
const data = JSON.parse(e.data);
console.log('🔵 SSE agent_output event received:', data.agent, data.message?.substring(0, 50));
this.handleAgentOutput(data);
});
this.eventSource.onerror = () => {
console.error('❌ SSE error');
this.updateConnectionStatus('disconnected');
this.eventSource.close();
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`🔄 Reconnecting in ${delay/1000}s...`);
setTimeout(() => this.connectSSE(), delay);
}
};
},
updateConnectionStatus(status) {
const el = document.getElementById('connection-status');
const text = document.getElementById('connection-text');
el.className = 'connection-status ' + status;
switch (status) {
case 'connected':
text.textContent = 'Live';
break;
case 'connecting':
text.textContent = 'Connecting...';
break;
case 'disconnected':
text.textContent = 'Disconnected';
break;
}
},
updateFromStatus(data) {
// Update stats
if (data.stats) {
document.getElementById('stat-total').textContent = data.stats.total_projects || 0;
document.getElementById('stat-completed').textContent = data.stats.completed_projects || 0;
document.getElementById('stat-ideas').textContent = data.stats.total_ideas || 0;
const inProgress = (data.stats.total_projects || 0) - (data.stats.completed_projects || 0);
document.getElementById('stat-inprogress').textContent = Math.max(0, inProgress);
}
// Update active project
if (data.active_project) {
this.updateProject(data.active_project);
}
// Update activity log (limited to most recent, no animation for initial load)
// Logs come in descending order (newest first), so we:
// 1. Take the first 10 (most recent)
// 2. Reverse them so oldest is processed first
// 3. Insert each at top, resulting in newest at top
if (data.recent_logs) {
data.recent_logs.slice(0, 10).reverse().forEach(log => {
this.addActivityItem({
agent: log.agent_name,
message: log.message,
type: log.log_type,
timestamp: log.timestamp
}, false); // false = no animation on initial load
});
}
this.updateLastUpdate();
},
// Handle orchestrator status update (running/idle)
handleStatusUpdate(data) {
const dot = document.getElementById('orchestrator-dot');
const status = document.getElementById('orchestrator-status');
if (data.orchestrator_running) {
dot.className = 'status-dot running';
status.textContent = 'Running';
} else {
dot.className = 'status-dot idle';
status.textContent = 'Idle';
}
},
// Legacy handler for bundled status updates (kept for backward compatibility)
updateOrchestratorStatus(data) {
// Handle orchestrator running state
this.handleStatusUpdate(data);
// Only update pipeline if we have a valid agent
if (data.current_agent && typeof data.current_agent === 'string') {
this.updatePipeline(data.current_agent);
}
if (data.current_project) {
this.updateProject(data.current_project);
}
// Update iteration count from DB (source of truth)
if (data.dev_test_iterations !== undefined) {
this.updateIterationCount(data.dev_test_iterations);
}
},
updateProject(project) {
const container = document.getElementById('project-info');
const badge = document.getElementById('project-badge');
if (!project) {
container.innerHTML = `
<div class="no-project">
<div class="no-project-icon">⏳</div>
<div>Waiting for a project to start...</div>
</div>
`;
badge.textContent = 'No Project';
badge.className = 'card-badge';
return;
}
container.className = 'project-info has-project';
badge.textContent = project.status;
badge.className = 'card-badge active';
container.innerHTML = `
<div class="project-name">${this.escapeHtml(project.name || 'Unnamed Project')}</div>
<span class="project-status-badge ${project.status}">${project.status}</span>
${project.current_agent ? `<div class="project-meta">Current Agent: <strong>${project.current_agent}</strong></div>` : ''}
`;
// Update pipeline based on status, but prefer current_agent if available
if (project.current_agent) {
this.updatePipeline(project.current_agent);
} else if (!this.lastKnownAgent && project.status && this.statusToAgent[project.status]) {
// Only fall back to status-based agent if we don't have a lastKnownAgent
// This prevents the pipeline from resetting to ideator when server returns incomplete data
this.updatePipeline(this.statusToAgent[project.status]);
}
},
updatePipeline(currentAgent) {
// Guard against null/undefined/empty agent names
if (!currentAgent || typeof currentAgent !== 'string') {
return; // Don't update pipeline with invalid agent
}
const pipelineStage = document.getElementById('pipeline-stage');
const progressBar = document.getElementById('pipeline-progress');
let activeIndex = this.agents.indexOf(currentAgent.toLowerCase());
if (activeIndex < 0) {
// Check if it's a completed state
if (currentAgent.toLowerCase() === 'completed') {
activeIndex = this.agents.length;
this.lastKnownAgent = 'completed';
} else if (this.lastKnownAgent) {
// Unknown agent - preserve last known state instead of resetting
// This prevents pipeline from jumping back to ideator on status updates
return;
} else {
// No last known agent and unknown current - truly idle, but don't reset
return;
}
} else {
// Valid agent - update last known
this.lastKnownAgent = currentAgent.toLowerCase();
}
// Update stage label
if (activeIndex >= 0 && activeIndex < this.agents.length) {
pipelineStage.textContent = `${activeIndex + 1}/${this.agents.length}`;
} else if (activeIndex >= this.agents.length) {
pipelineStage.textContent = 'Complete';
} else {
pipelineStage.textContent = 'Idle';
}
// Update progress bar
const totalWidth = document.querySelector('.pipeline').offsetWidth - 128; // Subtract padding
const progressWidth = activeIndex >= 0 ? (activeIndex / (this.agents.length - 1)) * totalWidth : 0;
progressBar.style.width = `${Math.min(progressWidth, totalWidth)}px`;
// Update agent nodes
this.agents.forEach((agent, idx) => {
const node = document.querySelector(`.agent-node[data-agent="${agent}"]`);
if (!node) return;
node.classList.remove('completed', 'active', 'pending');
if (idx < activeIndex) {
node.classList.add('completed');
} else if (idx === activeIndex) {
node.classList.add('active');
} else {
node.classList.add('pending');
}
});
},
updateIterationCount(count) {
document.getElementById('iteration-count').textContent = count;
},
addActivityItem(data, animate = true) {
const container = document.getElementById('activity-log');
// Remove empty state
const empty = container.querySelector('.activity-empty');
if (empty) empty.remove();
const item = document.createElement('div');
item.className = 'activity-item';
if (!animate) item.style.animation = 'none';
const time = data.timestamp ? new Date(data.timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
const dotClass = data.type || 'info';
item.innerHTML = `
<div class="activity-dot ${dotClass}"></div>
<div class="activity-content">
<div class="activity-agent">${this.escapeHtml(data.agent || 'System')}</div>
<div class="activity-message">${this.escapeHtml(data.message || '')}</div>
</div>
<div class="activity-time">${time}</div>
`;
// Insert at the top
container.insertBefore(item, container.firstChild);
this.activityItems.push(data);
// Limit items
if (this.activityItems.length > this.maxActivityItems) {
this.activityItems.shift();
const last = container.querySelector('.activity-item:last-child');
if (last) last.remove();
}
},
setAgentIdle() {
const statusBadge = document.getElementById('output-status');
this.isStreaming = false;
statusBadge.textContent = 'Waiting';
statusBadge.className = 'card-badge';
// Update streaming indicator
const content = document.querySelector('.agent-output-display-content');
if (content) {
content.classList.remove('streaming');
}
const status = document.querySelector('.agent-output-display-status');
if (status) {
status.textContent = 'Completed';
status.classList.remove('running');
}
},
handleAgentStarted(data) {
this.updatePipeline(data.agent);
// Reset output for new agent
this.currentAgent = data.agent;
this.currentAgentOutput = '';
this.isStreaming = true;
this.initAgentOutputDisplay(data.agent);
this.addActivityItem({
agent: data.agent,
message: data.message || `${data.agent} started`,
type: 'info',
timestamp: data.timestamp
});
},
handleAgentOutput(data) {
console.log('📝 Agent output received:', data.agent, 'len:', data.message?.length, 'streaming:', data.data?.streaming);
// Append streaming output content in real-time
if (data.agent && data.message) {
// Initialize display if not already showing this agent
if (data.agent !== this.currentAgent || !this.isStreaming) {
// New agent or wasn't streaming, initialize display
this.currentAgent = data.agent;
this.currentAgentOutput = '';
this.isStreaming = true;
this.initAgentOutputDisplay(data.agent);
}
// Always append - real-time streaming sends chunks
this.currentAgentOutput += data.message;
this.updateAgentOutputContent();
}
},
initAgentOutputDisplay(agentName) {
const container = document.getElementById('agent-output-main');
const statusBadge = document.getElementById('output-status');
const iconSrc = this.agentIcons[agentName.toLowerCase()];
const fallbackEmoji = this.fallbackEmojis[agentName.toLowerCase()] || '🤖';
let iconHtml;
if (iconSrc) {
iconHtml = `<img src="${iconSrc}" alt="${agentName}" onerror="this.outerHTML='${fallbackEmoji}'">`;
} else {
iconHtml = fallbackEmoji;
}
statusBadge.textContent = 'Running';
statusBadge.className = 'card-badge active';
container.innerHTML = `
<div class="agent-output-display-header">
<div class="agent-output-display-icon">${iconHtml}</div>
<div class="agent-output-display-info">
<div class="agent-output-display-name">${agentName}</div>
<div class="agent-output-display-status running">Processing...</div>
</div>
</div>
<div class="agent-output-display-content streaming" id="agent-output-content"></div>
`;
},
updateAgentOutputContent() {
const contentEl = document.getElementById('agent-output-content');
if (contentEl) {
contentEl.textContent = this.currentAgentOutput;
// Auto-scroll to bottom
const container = document.getElementById('agent-output-main');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
},
handleAgentCompleted(data) {
this.setAgentIdle();
this.addActivityItem({
agent: data.agent,
message: data.message || `${data.agent} completed`,
type: 'output',
timestamp: data.timestamp
});
},
handleAgentError(data) {
this.setAgentIdle();
this.addActivityItem({
agent: data.agent,
message: data.message || `Error in ${data.agent}`,
type: 'error',
timestamp: data.timestamp
});
},
handleWorkflowStarted(data) {
this.resetPipeline();
this.addActivityItem({
agent: 'SYSTEM',
message: 'New workflow started',
type: 'info',
timestamp: data.timestamp
});
},
handleWorkflowCompleted(data) {
this.lastKnownAgent = 'completed';
this.agents.forEach(agent => {
const node = document.querySelector(`.agent-node[data-agent="${agent}"]`);
if (node) {
node.classList.remove('active', 'pending');
node.classList.add('completed');
}
});
document.getElementById('pipeline-stage').textContent = 'Complete';
this.setAgentIdle();
this.addActivityItem({
agent: 'SYSTEM',
message: `Workflow completed! Project: ${data.data?.project_name || 'Unknown'}`,
type: 'output',
timestamp: data.timestamp
});
// Refresh stats
this.fetchInitialData();
},
resetPipeline() {
this.lastKnownAgent = null; // Clear last known agent on reset
this.agents.forEach(agent => {
const node = document.querySelector(`.agent-node[data-agent="${agent}"]`);
if (node) {
node.classList.remove('completed', 'active');
node.classList.add('pending');
}
});
document.getElementById('pipeline-progress').style.width = '0';
document.getElementById('pipeline-stage').textContent = 'Idle';
document.getElementById('iteration-count').textContent = '0';
// Reset main output display
const container = document.getElementById('agent-output-main');
container.innerHTML = `
<div class="agent-output-empty">
<div class="agent-output-empty-icon">🤖</div>
<div class="agent-output-empty-text">Waiting for agent activity...</div>
</div>
`;
document.getElementById('output-status').textContent = 'Waiting';
document.getElementById('output-status').className = 'card-badge';
},
updateLastUpdate() {
document.getElementById('last-update').textContent =
`Updated: ${new Date().toLocaleTimeString()}`;
},
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// Initialize on load
document.addEventListener('DOMContentLoaded', () => Dashboard.init());
</script>
</body>
</html>