1536 lines
55 KiB
HTML
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>
|