rdev/ideas/aeres/index.html
jordan d69da6d627 feat: add structured logging infrastructure and SDLC extensions
Major changes:
- Add internal/logging package with field constants, context propagation,
  sensitive data auto-redaction, and per-component log levels
- Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.)
- Extend SDLC with callback handlers, generate endpoints, and executor
- Add new cookbook trees for aeries and slackpath progression
- Add skeleton templates for queue, realtime, and microservices
- Add worker component template with async job processing
- Refactor services and handlers to use new logging infrastructure
- Split component.go into component_infra.go and component_listing.go

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:56:04 -07:00

1264 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>Agent Creator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0a0f;
color: #e0e0e0;
min-height: 100vh;
}
.container { max-width: 1600px; margin: 0 auto; padding: 40px 24px; }
h1 { font-size: 32px; font-weight: 300; margin-bottom: 8px; color: #888; }
h1 span { font-weight: 700; color: #fff; font-style: italic; }
h2 { font-size: 20px; font-weight: 600; margin-bottom: 16px; color: #888; }
.subtitle { color: #666; margin-bottom: 32px; }
/* Layout */
.main-grid { display: grid; grid-template-columns: 360px 1fr; gap: 40px; }
@media (max-width: 900px) { .main-grid { grid-template-columns: 1fr; } }
/* Form */
.form-section { background: #12121a; border-radius: 16px; padding: 24px; margin-bottom: 24px; }
.form-group { margin-bottom: 20px; }
.form-group:last-child { margin-bottom: 0; }
label { display: block; font-size: 13px; color: #888; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
textarea, input[type="text"] {
width: 100%; background: #0a0a0f; border: 1px solid #2a2a3e; border-radius: 12px;
padding: 14px 16px; color: #fff; font-size: 15px; resize: none;
}
textarea:focus, input:focus { outline: none; border-color: #ff6b9d; }
textarea::placeholder, input::placeholder { color: #555; }
/* Gender Pills */
.gender-pills { display: flex; gap: 12px; }
.gender-pill {
flex: 1; background: #0a0a0f; border: 1px solid #2a2a3e; border-radius: 10px;
padding: 14px; text-align: center; cursor: pointer; transition: all 0.2s; color: #888;
}
.gender-pill:hover { border-color: #444; color: #fff; }
.gender-pill.selected { border-color: #ff6b9d; background: rgba(255, 107, 157, 0.1); color: #fff; }
/* Button */
.btn-primary {
width: 100%; padding: 16px 24px; border-radius: 12px; font-size: 16px; font-weight: 600;
cursor: pointer; transition: all 0.2s; border: none;
background: linear-gradient(135deg, #ff6b9d, #c44569); color: #fff;
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(255, 107, 157, 0.3); }
.btn-primary:disabled { background: #2a2a3e; color: #555; cursor: not-allowed; transform: none; box-shadow: none; }
/* Progress */
.progress-section { display: none; margin-bottom: 24px; }
.progress-section.active { display: block; }
.progress-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.progress-spinner { width: 20px; height: 20px; border: 2px solid #1a1a2e; border-top-color: #ff6b9d; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.progress-steps { background: #12121a; border-radius: 12px; padding: 16px; }
.step { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid #1a1a2e; }
.step:last-child { border-bottom: none; }
.step-icon { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; }
.step-icon.pending { background: #1a1a2e; color: #555; }
.step-icon.active { background: #ff6b9d; color: #fff; }
.step-icon.done { background: #4ade80; color: #fff; }
.step-label { font-size: 14px; }
.step-label.pending { color: #555; }
.step-label.active { color: #fff; }
.step-label.done { color: #4ade80; }
/* Jobs Panel */
.jobs-panel { background: #12121a; border-radius: 16px; padding: 20px; margin-bottom: 24px; }
.jobs-panel h3 { font-size: 16px; margin-bottom: 16px; color: #888; }
.job-item { display: flex; align-items: center; gap: 12px; padding: 12px; background: #0a0a0f; border-radius: 10px; margin-bottom: 8px; }
.job-item:last-child { margin-bottom: 0; }
.job-type { width: 70px; font-size: 12px; text-transform: uppercase; color: #888; }
.job-agent { flex: 1; font-size: 13px; color: #aaa; }
.job-status { font-size: 12px; padding: 4px 8px; border-radius: 4px; }
.job-status.pending { background: #2a2a3e; color: #888; }
.job-status.generating { background: rgba(255, 107, 157, 0.2); color: #ff6b9d; }
.job-status.complete { background: rgba(74, 222, 128, 0.2); color: #4ade80; }
.job-status.error { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.job-progress { width: 60px; text-align: right; font-size: 12px; color: #888; }
.no-jobs { text-align: center; padding: 20px; color: #555; font-size: 14px; }
/* Agents Grid */
.agents-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
.agent-card { background: #12121a; border-radius: 16px; overflow: hidden; cursor: pointer; transition: all 0.2s; }
.agent-card:hover { transform: translateY(-4px); box-shadow: 0 8px 30px rgba(0,0,0,0.3); }
.agent-header { position: relative; }
.agent-banner { aspect-ratio: 3/1; background: linear-gradient(135deg, #1a1a2e, #2a2a3e); background-size: cover; background-position: center top; }
.agent-avatar { width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #ff6b9d, #c44569); margin: -28px 0 0 16px; border: 3px solid #12121a; background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; font-size: 22px; color: #fff; }
.agent-info { padding: 12px 16px 16px; }
.agent-name { font-size: 18px; font-weight: 600; margin-bottom: 2px; }
.agent-handle { font-size: 13px; color: #888; margin-bottom: 12px; }
.agent-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.agent-tag { font-size: 11px; padding: 3px 8px; background: rgba(255, 107, 157, 0.1); border-radius: 10px; color: #ff6b9d; }
.agent-media-count { font-size: 12px; color: #666; }
.no-agents { text-align: center; padding: 60px 20px; color: #555; }
/* Infinite scroll loading indicator */
.loading-more { text-align: center; padding: 30px 20px; color: #888; }
.loading-more .spinner { display: inline-block; width: 24px; height: 24px; border: 2px solid #1a1a2e; border-top-color: #ff6b9d; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 12px; vertical-align: middle; }
/* Agent Modal */
.modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 1000; overflow-y: auto; padding: 40px 20px; }
.modal.active { display: block; }
.modal-content { max-width: 900px; margin: 0 auto; background: #12121a; border-radius: 20px; overflow: hidden; }
.modal-close { position: absolute; top: 20px; right: 20px; width: 40px; height: 40px; background: rgba(0,0,0,0.5); border: none; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; z-index: 10; }
.modal-header { position: relative; }
.modal-banner { aspect-ratio: 3/1; background: linear-gradient(135deg, #1a1a2e, #2a2a3e); background-size: cover; background-position: center top; }
.modal-avatar { width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(135deg, #ff6b9d, #c44569); margin: -50px 0 0 30px; border: 4px solid #12121a; background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; font-size: 40px; color: #fff; }
.modal-info { padding: 16px 30px 30px; }
.modal-name { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
.modal-handle { font-size: 16px; color: #888; margin-bottom: 16px; }
.modal-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; }
.modal-section { margin-bottom: 24px; }
.modal-section h4 { font-size: 14px; color: #ff6b9d; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.modal-section p { color: #aaa; line-height: 1.6; }
.modal-gallery { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.gallery-item { position: relative; }
.gallery-item img { width: 100%; aspect-ratio: 3/4; object-fit: cover; border-radius: 8px; background: #1a1a2e; cursor: pointer; transition: transform 0.2s; }
.gallery-item img:hover { transform: scale(1.02); }
.gallery-delete { position: absolute; top: 4px; right: 4px; width: 24px; height: 24px; background: rgba(239, 68, 68, 0.9); border: none; border-radius: 50%; color: #fff; font-size: 14px; cursor: pointer; opacity: 0; transition: opacity 0.2s; display: flex; align-items: center; justify-content: center; }
.gallery-item:hover .gallery-delete { opacity: 1; }
.gallery-delete:hover { background: #ef4444; }
.modal-videos { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.modal-video { width: 100%; border-radius: 12px; background: #0a0a0f; }
.trait-list { display: flex; flex-wrap: wrap; gap: 8px; }
.trait-item { background: #1a1a2e; padding: 6px 12px; border-radius: 8px; font-size: 13px; }
/* Modal Actions */
.modal-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 24px; padding-bottom: 24px; border-bottom: 1px solid #1a1a2e; }
.action-btn { padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: 1px solid #2a2a3e; background: #0a0a0f; color: #aaa; }
.action-btn:hover { border-color: #ff6b9d; color: #ff6b9d; }
.action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.action-btn.generating { border-color: #ff6b9d; color: #ff6b9d; }
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.section-header h4 { margin: 0; }
.add-btn { padding: 6px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; border: 1px solid #2a2a3e; background: transparent; color: #888; }
.add-btn:hover { border-color: #ff6b9d; color: #ff6b9d; }
/* Error */
.error-message { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 12px; padding: 16px; margin-bottom: 24px; color: #ef4444; display: none; }
.error-message.active { display: block; }
/* Success toast */
.success-toast { position: fixed; bottom: 20px; right: 20px; background: #4ade80; color: #0a0a0f; padding: 16px 24px; border-radius: 12px; font-weight: 600; transform: translateY(100px); opacity: 0; transition: all 0.3s; z-index: 1001; }
.success-toast.active { transform: translateY(0); opacity: 1; }
/* Fullscreen viewer */
.fullscreen-viewer { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.95); z-index: 2000; align-items: center; justify-content: center; }
.fullscreen-viewer.active { display: flex; }
.fullscreen-image { max-width: 90vw; max-height: 90vh; object-fit: contain; border-radius: 8px; }
.fullscreen-close { position: absolute; top: 20px; right: 20px; width: 48px; height: 48px; background: rgba(255,255,255,0.1); border: none; border-radius: 50%; color: #fff; font-size: 28px; cursor: pointer; }
.fullscreen-close:hover { background: rgba(255,255,255,0.2); }
.fullscreen-nav { position: absolute; top: 50%; transform: translateY(-50%); width: 48px; height: 48px; background: rgba(255,255,255,0.1); border: none; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; }
.fullscreen-nav:hover { background: rgba(255,255,255,0.2); }
.fullscreen-prev { left: 20px; }
.fullscreen-next { right: 20px; }
.fullscreen-counter { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: #888; font-size: 14px; }
/* Selection mode */
.selection-checkbox {
display: none;
position: absolute;
top: 8px;
left: 8px;
width: 24px;
height: 24px;
background: rgba(0,0,0,0.6);
border: 2px solid #fff;
border-radius: 50%;
cursor: pointer;
align-items: center;
justify-content: center;
font-size: 14px;
color: #fff;
z-index: 5;
transition: all 0.2s;
}
.selection-mode .selection-checkbox { display: flex; }
.selection-checkbox:hover { background: rgba(255, 107, 157, 0.5); }
.selection-checkbox.selected {
background: #ff6b9d;
border-color: #ff6b9d;
}
.selection-checkbox.selected::after { content: '✓'; }
/* Video checkbox (reuse same styling) */
.video-item { position: relative; }
.video-delete { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; background: rgba(239, 68, 68, 0.9); border: none; border-radius: 50%; color: #fff; font-size: 16px; cursor: pointer; opacity: 0; transition: opacity 0.2s; display: flex; align-items: center; justify-content: center; z-index: 5; }
.video-item:hover .video-delete { opacity: 1; }
.video-delete:hover { background: #ef4444; }
.video-checkbox {
display: none;
position: absolute;
top: 8px;
left: 8px;
width: 24px;
height: 24px;
background: rgba(0,0,0,0.6);
border: 2px solid #fff;
border-radius: 50%;
cursor: pointer;
align-items: center;
justify-content: center;
font-size: 14px;
color: #fff;
z-index: 5;
transition: all 0.2s;
}
.selection-mode .video-checkbox { display: flex; }
.video-checkbox:hover { background: rgba(255, 107, 157, 0.5); }
.video-checkbox.selected {
background: #ff6b9d;
border-color: #ff6b9d;
}
.video-checkbox.selected::after { content: '✓'; }
/* Fix Panel */
.fix-panel {
display: none;
background: #0a0a0f;
border: 1px solid #2a2a3e;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.fix-panel.active {
display: block;
}
.fix-panel h4 {
font-size: 14px;
color: #ff6b9d;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.fix-option {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 12px 0;
border-bottom: 1px solid #1a1a2e;
}
.fix-option:last-child {
border-bottom: none;
}
.fix-option label {
flex: 0 0 140px;
font-size: 13px;
color: #888;
margin: 0;
}
.fix-option input[type="text"] {
flex: 1;
min-width: 120px;
padding: 8px 12px;
font-size: 13px;
}
.fix-option select {
flex: 1;
min-width: 120px;
background: #0a0a0f;
border: 1px solid #2a2a3e;
border-radius: 8px;
padding: 8px 12px;
color: #fff;
font-size: 13px;
}
.fix-option select:focus {
outline: none;
border-color: #ff6b9d;
}
.fix-btn {
padding: 8px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
border: 1px solid #2a2a3e;
background: transparent;
color: #888;
transition: all 0.2s;
}
.fix-btn:hover {
border-color: #ff6b9d;
color: #ff6b9d;
}
.fix-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.fix-btn-group {
display: flex;
gap: 8px;
}
.fix-hint {
width: 100%;
font-size: 11px;
color: #666;
margin-top: 4px;
}
/* Bulk action bar */
.bulk-action-bar {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #1a1a2e;
border-top: 1px solid #2a2a3e;
padding: 16px 24px;
z-index: 1100;
align-items: center;
justify-content: space-between;
}
.bulk-action-bar.active { display: flex; }
.bulk-count { color: #aaa; font-size: 14px; }
.bulk-count span { color: #ff6b9d; font-weight: 600; }
.bulk-actions { display: flex; gap: 12px; }
.bulk-btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.bulk-btn-cancel {
background: #2a2a3e;
color: #aaa;
}
.bulk-btn-cancel:hover { background: #3a3a4e; color: #fff; }
.bulk-btn-delete {
background: #ef4444;
color: #fff;
}
.bulk-btn-delete:hover { background: #dc2626; }
.bulk-btn-delete:disabled { background: #555; cursor: not-allowed; }
</style>
</head>
<body>
<div class="container">
<h1>i want a <span>hot...</span></h1>
<p class="subtitle">describe your ideal AI companion and we'll create them</p>
<div class="main-grid">
<!-- Left Column: Form & Jobs -->
<div>
<div class="error-message" id="error"></div>
<!-- Form -->
<div id="form-section">
<div class="form-section">
<div class="form-group">
<label>Description</label>
<textarea id="description" rows="3" placeholder="mysterious woman with dark hair who loves poetry..."></textarea>
</div>
<div class="form-group">
<label>Gender</label>
<div class="gender-pills">
<div class="gender-pill selected" data-gender="female">Female</div>
<div class="gender-pill" data-gender="male">Male</div>
<div class="gender-pill" data-gender="non-binary">Non-binary</div>
</div>
</div>
</div>
<button class="btn-primary" id="create-btn">Create Agent</button>
</div>
<!-- Progress -->
<div class="progress-section" id="progress-section">
<div class="progress-header">
<div class="progress-spinner"></div>
<span>Creating agent...</span>
</div>
<div class="progress-steps">
<div class="step" data-step="personality">
<div class="step-icon pending">1</div>
<div class="step-label pending">Crafting personality</div>
</div>
<div class="step" data-step="appearance">
<div class="step-icon pending">2</div>
<div class="step-label pending">Designing appearance</div>
</div>
<div class="step" data-step="identity">
<div class="step-icon pending">3</div>
<div class="step-label pending">Creating identity</div>
</div>
<div class="step" data-step="tags">
<div class="step-icon pending">4</div>
<div class="step-label pending">Generating tags</div>
</div>
</div>
</div>
<!-- Jobs Panel -->
<div class="jobs-panel">
<h3>Media Generation Jobs</h3>
<div id="jobs-list">
<div class="no-jobs">No active jobs</div>
</div>
</div>
</div>
<!-- Right Column: Agents -->
<div>
<h2>Created Agents</h2>
<div class="agents-grid" id="agents-grid">
<div class="no-agents">No agents created yet. Create your first one!</div>
</div>
</div>
</div>
</div>
<!-- Agent Modal -->
<div class="modal" id="agent-modal">
<div class="modal-content">
<button class="modal-close" onclick="closeModal()">&times;</button>
<div class="modal-header">
<div class="modal-banner" id="modal-banner"></div>
<div class="modal-avatar" id="modal-avatar"></div>
</div>
<div class="modal-info">
<div class="modal-name" id="modal-name"></div>
<div class="modal-handle" id="modal-handle"></div>
<div class="modal-tags" id="modal-tags"></div>
<div class="modal-actions" id="modal-actions">
<button class="action-btn" onclick="generateMedia('avatar')">Generate Avatar</button>
<button class="action-btn" onclick="generateMedia('banner')">Generate Banner</button>
<button class="action-btn" onclick="generateMedia('images')">Add Images</button>
<button class="action-btn" onclick="generateMedia('video')">Generate Video</button>
<button class="action-btn" id="fix-issues-btn" onclick="toggleFixPanel()">Fix Issues</button>
<button class="action-btn" id="select-multiple-btn" onclick="toggleSelectionMode()" style="margin-left: auto;">Select Multiple</button>
</div>
<!-- Fix Panel -->
<div class="fix-panel" id="fix-panel">
<h4>Fix Issues</h4>
<!-- Position Regeneration -->
<div class="fix-option">
<label>Regenerate Positions</label>
<input type="text" id="fix-positions" placeholder="e.g., 4, 9, 15">
<button class="fix-btn" onclick="regeneratePositions()">Regenerate</button>
<div class="fix-hint">Enter position numbers (1-75) separated by commas</div>
</div>
<!-- Eye Color -->
<div class="fix-option">
<label>Eye Color Intensity</label>
<div class="fix-btn-group">
<button class="fix-btn" onclick="modifyPersona('tone_eye_color', 'subtle')">Subtle</button>
<button class="fix-btn" onclick="modifyPersona('tone_eye_color', 'natural')">Natural</button>
</div>
<div class="fix-hint">Tone down vivid/striking eye color descriptions in persona spec</div>
</div>
<!-- Hair/Brows -->
<div class="fix-option">
<label>Hair/Brow Match</label>
<button class="fix-btn" onclick="modifyPersona('align_hair_brows')">Align Colors</button>
<div class="fix-hint">Ensure brow color description matches hair color</div>
</div>
<!-- Banner Style -->
<div class="fix-option">
<label>Banner Style</label>
<select id="banner-style">
<option value="">Auto</option>
<option value="lifestyle">Lifestyle</option>
<option value="portrait">Portrait</option>
<option value="scenic">Scenic</option>
<option value="artistic">Artistic</option>
</select>
<button class="fix-btn" onclick="regenerateBanner()">Regenerate</button>
<div class="fix-hint">Regenerate banner with a specific style hint</div>
</div>
</div>
<div class="modal-actions" id="modal-anchor-missing" style="display:none;">
<div style="color: #f59e0b; margin-bottom: 12px;">⚠️ Anchor image missing - regenerate to enable other actions</div>
<button class="action-btn" style="background: #f59e0b;" onclick="generateMedia('anchor')">Regenerate Anchor</button>
</div>
<div class="modal-section" id="modal-video-section" style="display:none;">
<h4>Videos</h4>
<div class="modal-videos" id="modal-videos"></div>
</div>
<div class="modal-section" id="modal-gallery-section" style="display:none;">
<h4>Gallery</h4>
<div class="modal-gallery" id="modal-gallery"></div>
</div>
</div>
</div>
</div>
<!-- Success Toast -->
<div class="success-toast" id="success-toast"></div>
<!-- Bulk Action Bar -->
<div class="bulk-action-bar" id="bulk-action-bar">
<span class="bulk-count"><span id="bulk-count">0</span> selected</span>
<div class="bulk-actions">
<button class="bulk-btn bulk-btn-cancel" onclick="exitSelectionMode()">Cancel</button>
<button class="bulk-btn bulk-btn-delete" id="bulk-delete-btn" onclick="bulkDelete()" disabled>Delete Selected</button>
</div>
</div>
<!-- Fullscreen Gallery Viewer -->
<div class="fullscreen-viewer" id="fullscreen-viewer">
<button class="fullscreen-close" onclick="closeFullscreen()">&times;</button>
<button class="fullscreen-nav fullscreen-prev" onclick="navigateFullscreen(-1)">&larr;</button>
<img class="fullscreen-image" id="fullscreen-image" src="" alt="Full size">
<button class="fullscreen-nav fullscreen-next" onclick="navigateFullscreen(1)">&rarr;</button>
<div class="fullscreen-counter" id="fullscreen-counter"></div>
</div>
<script>
let ws = null;
let selectedGender = 'female';
let agents = {};
let jobsByAgent = {};
// Selection mode state
let selectionMode = false;
let selectedImages = new Set();
let selectedVideos = new Set();
// Pagination state
const PAGE_SIZE = 20;
let currentOffset = 0;
let hasMore = true;
let loadingMore = false;
let totalAgents = 0;
document.addEventListener('DOMContentLoaded', () => {
connectWebSocket();
loadAgents();
setupEventListeners();
setupInfiniteScroll();
});
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
ws.onopen = () => console.log('WebSocket connected');
ws.onclose = () => { console.log('WebSocket disconnected'); setTimeout(connectWebSocket, 2000); };
ws.onerror = (err) => console.error('WebSocket error:', err);
ws.onmessage = (event) => handleMessage(JSON.parse(event.data));
}
function handleMessage(msg) {
const payload = msg.payload;
switch (msg.type) {
case 'step':
setStepActive(payload.step);
break;
case 'stream':
// Could show streaming text
break;
case 'step_done':
setStepDone(payload.step);
break;
case 'complete':
showResult(payload.agent);
break;
case 'error':
if (payload.code === 'ANCHOR_MISSING') {
showAnchorMissing();
} else if (payload.code === 'DUPLICATE_AGENT') {
showError(`${payload.message}. <a href="#" onclick="event.preventDefault(); closeModal(); setTimeout(() => openModal('${payload.existing_id}'), 100)">View existing agent</a>`);
resetForm();
} else if (payload.code === 'NAME_TOO_LONG') {
showError(payload.message);
resetForm();
} else {
showError(payload.message || payload);
resetForm();
}
break;
case 'agent_created':
agents[payload.id] = payload;
totalAgents++;
currentOffset++; // Account for the new agent in pagination
renderAgents();
showToast(`${payload.name} created! Generating media...`);
break;
case 'agent_updated':
agents[payload.id] = payload;
renderAgents();
updateModalIfOpen(payload);
break;
case 'job_update':
updateJob(payload);
break;
case 'job_removed':
removeJob(payload.id);
break;
case 'image_deleted':
showToast('Image deleted');
break;
case 'video_deleted':
showToast('Video deleted');
break;
case 'bulk_deleted':
const imgCount = payload.images_deleted || 0;
const vidCount = payload.videos_deleted || 0;
const parts = [];
if (imgCount > 0) parts.push(`${imgCount} image${imgCount > 1 ? 's' : ''}`);
if (vidCount > 0) parts.push(`${vidCount} video${vidCount > 1 ? 's' : ''}`);
showToast(`Deleted ${parts.join(' and ')}`);
exitSelectionMode();
break;
case 'positions_regenerating':
showToast(`Regenerating positions: ${payload.positions.join(', ')}...`);
break;
case 'persona_modified':
if (payload.modified) {
showToast(payload.description);
} else {
showToast('No changes needed');
}
break;
case 'banner_regenerating':
const styleMsg = payload.style ? ` (${payload.style} style)` : '';
showToast(`Regenerating banner${styleMsg}...`);
break;
}
}
function setupEventListeners() {
document.querySelectorAll('.gender-pill').forEach(pill => {
pill.addEventListener('click', () => {
document.querySelectorAll('.gender-pill').forEach(p => p.classList.remove('selected'));
pill.classList.add('selected');
selectedGender = pill.dataset.gender;
});
});
document.getElementById('create-btn').addEventListener('click', createAgent);
document.getElementById('agent-modal').addEventListener('click', (e) => {
if (e.target.id === 'agent-modal') closeModal();
});
}
function createAgent() {
const description = document.getElementById('description').value.trim();
if (!description) { showError('Please enter a description'); return; }
if (!ws || ws.readyState !== WebSocket.OPEN) { showError('Not connected'); return; }
document.getElementById('form-section').style.display = 'none';
document.getElementById('progress-section').classList.add('active');
hideError();
resetSteps();
ws.send(JSON.stringify({ type: 'create', payload: { description, gender: selectedGender } }));
}
function resetSteps() {
document.querySelectorAll('.step').forEach(step => {
step.querySelector('.step-icon').className = 'step-icon pending';
step.querySelector('.step-label').className = 'step-label pending';
});
}
function setStepActive(stepName) {
const step = document.querySelector(`.step[data-step="${stepName}"]`);
if (step) {
step.querySelector('.step-icon').className = 'step-icon active';
step.querySelector('.step-icon').textContent = '...';
step.querySelector('.step-label').className = 'step-label active';
}
}
function setStepDone(stepName) {
const step = document.querySelector(`.step[data-step="${stepName}"]`);
if (step) {
step.querySelector('.step-icon').className = 'step-icon done';
step.querySelector('.step-icon').innerHTML = '&#10003;';
step.querySelector('.step-label').className = 'step-label done';
}
}
function showResult(agent) {
document.getElementById('progress-section').classList.remove('active');
document.getElementById('form-section').style.display = 'block';
document.getElementById('description').value = '';
agents[agent.id] = agent;
renderAgents();
}
function resetForm() {
document.getElementById('form-section').style.display = 'block';
document.getElementById('progress-section').classList.remove('active');
}
function showError(message) {
const el = document.getElementById('error');
el.innerHTML = message; // Use innerHTML to support links
el.classList.add('active');
}
function hideError() {
document.getElementById('error').classList.remove('active');
}
function showAnchorMissing() {
document.getElementById('modal-actions').style.display = 'none';
document.getElementById('modal-anchor-missing').style.display = 'block';
}
function hideAnchorMissing() {
document.getElementById('modal-actions').style.display = 'flex';
document.getElementById('modal-anchor-missing').style.display = 'none';
}
function showToast(message) {
const toast = document.getElementById('success-toast');
toast.textContent = message;
toast.classList.add('active');
setTimeout(() => toast.classList.remove('active'), 3000);
}
function updateJob(job) {
if (!jobsByAgent[job.agent_id]) jobsByAgent[job.agent_id] = {};
jobsByAgent[job.agent_id][job.type] = job;
renderJobs();
}
function removeJob(jobId) {
Object.keys(jobsByAgent).forEach(agentId => {
Object.keys(jobsByAgent[agentId]).forEach(type => {
if (jobsByAgent[agentId][type].id === jobId) {
delete jobsByAgent[agentId][type];
}
});
if (Object.keys(jobsByAgent[agentId]).length === 0) {
delete jobsByAgent[agentId];
}
});
renderJobs();
}
function renderJobs() {
const container = document.getElementById('jobs-list');
const allJobs = [];
Object.values(jobsByAgent).forEach(agentJobs => {
Object.values(agentJobs).forEach(job => allJobs.push(job));
});
// Sort: generating first, then pending, then complete
const order = { generating: 0, pending: 1, complete: 2, error: 3 };
allJobs.sort((a, b) => (order[a.status] || 9) - (order[b.status] || 9));
if (allJobs.length === 0) {
container.innerHTML = '<div class="no-jobs">No active jobs</div>';
return;
}
container.innerHTML = allJobs.map(job => `
<div class="job-item">
<div class="job-type">${job.type}</div>
<div class="job-agent">${job.agent_name}</div>
<div class="job-status ${job.status}">${job.status}</div>
<div class="job-progress">${job.type === 'gallery' ? job.progress + '%' : ''}</div>
</div>
`).join('');
}
async function loadAgents(append = false) {
if (loadingMore) return;
if (append && !hasMore) return;
loadingMore = true;
showLoadingIndicator();
try {
const response = await fetch(`/agents?limit=${PAGE_SIZE}&offset=${currentOffset}`);
const data = await response.json();
data.agents.forEach(agent => agents[agent.id] = agent);
hasMore = data.has_more;
totalAgents = data.total;
currentOffset += data.agents.length;
renderAgents();
} catch (err) {
console.error('Failed to load agents:', err);
} finally {
loadingMore = false;
hideLoadingIndicator();
}
}
function setupInfiniteScroll() {
window.addEventListener('scroll', () => {
if (loadingMore || !hasMore) return;
const scrollBottom = window.innerHeight + window.scrollY;
const threshold = document.body.offsetHeight - 500;
if (scrollBottom >= threshold) {
loadAgents(true);
}
});
}
function showLoadingIndicator() {
let indicator = document.getElementById('loading-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'loading-indicator';
indicator.className = 'loading-more';
indicator.innerHTML = '<span class="spinner"></span>Loading more agents...';
document.getElementById('agents-grid').after(indicator);
}
indicator.style.display = 'block';
}
function hideLoadingIndicator() {
const indicator = document.getElementById('loading-indicator');
if (indicator) {
indicator.style.display = 'none';
}
}
function renderAgents() {
const grid = document.getElementById('agents-grid');
const list = Object.values(agents).sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (list.length === 0) {
grid.innerHTML = '<div class="no-agents">No agents created yet. Create your first one!</div>';
return;
}
grid.innerHTML = list.map(agent => `
<div class="agent-card" onclick="openModal('${agent.id}')">
<div class="agent-header">
<div class="agent-banner" style="${agent.banner_url ? `background-image: url(${agent.banner_url})` : ''}"></div>
<div class="agent-avatar" style="${agent.avatar_url ? `background-image: url(${agent.avatar_url})` : ''}">${agent.avatar_url ? '' : agent.name.charAt(0)}</div>
</div>
<div class="agent-info">
<div class="agent-name">${agent.name}</div>
<div class="agent-handle">@${agent.handle}</div>
<div class="agent-tags">${(agent.tags || []).slice(0, 4).map(t => `<span class="agent-tag">${t}</span>`).join('')}</div>
<div class="agent-media-count">
${agent.images?.length || 0} images
${agent.videos?.length ? `${agent.videos.length} video${agent.videos.length > 1 ? 's' : ''}` : ''}
</div>
</div>
</div>
`).join('');
}
let currentModalAgentId = null;
function openModal(agentId) {
currentModalAgentId = agentId;
const agent = agents[agentId];
if (!agent) return;
// Reset anchor missing state
hideAnchorMissing();
// Update selection mode UI (preserve state if re-rendering same agent)
const modalContent = document.querySelector('.modal-content');
const selectBtn = document.getElementById('select-multiple-btn');
if (selectionMode) {
modalContent.classList.add('selection-mode');
selectBtn.textContent = 'Cancel Selection';
selectBtn.style.borderColor = '#ff6b9d';
selectBtn.style.color = '#ff6b9d';
} else {
modalContent.classList.remove('selection-mode');
selectBtn.textContent = 'Select Multiple';
selectBtn.style.borderColor = '';
selectBtn.style.color = '';
}
document.getElementById('modal-banner').style.backgroundImage = agent.banner_url ? `url(${agent.banner_url})` : '';
const avatarEl = document.getElementById('modal-avatar');
if (agent.avatar_url) {
avatarEl.style.backgroundImage = `url(${agent.avatar_url})`;
avatarEl.textContent = '';
} else {
avatarEl.style.backgroundImage = '';
avatarEl.textContent = agent.name.charAt(0);
}
document.getElementById('modal-name').textContent = agent.name;
document.getElementById('modal-handle').textContent = '@' + agent.handle;
document.getElementById('modal-tags').innerHTML = (agent.tags || []).map(t => `<span class="agent-tag">${t}</span>`).join('');
// Videos
const videoSection = document.getElementById('modal-video-section');
const videosContainer = document.getElementById('modal-videos');
if (agent.videos && agent.videos.length > 0) {
videosContainer.innerHTML = agent.videos.map(url => `
<div class="video-item">
<div class="video-checkbox ${selectedVideos.has(url) ? 'selected' : ''}" onclick="toggleVideoSelection('${url}', event)"></div>
<button class="video-delete" onclick="event.stopPropagation(); deleteVideo('${url}')" title="Delete video">&times;</button>
<video class="modal-video" controls onclick="handleVideoClick(event)">
<source src="${url}" type="video/mp4">
</video>
</div>
`).join('');
videoSection.style.display = 'block';
} else {
videoSection.style.display = 'none';
}
// Gallery
const gallerySection = document.getElementById('modal-gallery-section');
const galleryEl = document.getElementById('modal-gallery');
if (agent.images && agent.images.length > 0) {
galleryEl.innerHTML = agent.images.map((url, index) => `
<div class="gallery-item">
<div class="selection-checkbox ${selectedImages.has(url) ? 'selected' : ''}" onclick="toggleImageSelection('${url}', event)"></div>
<img src="${url}" alt="Gallery" loading="lazy" onclick="handleGalleryClick(${index}, '${url}', event)">
<button class="gallery-delete" onclick="event.stopPropagation(); deleteImage('${url}')" title="Delete image">&times;</button>
</div>
`).join('');
gallerySection.style.display = 'block';
} else {
gallerySection.style.display = 'none';
}
document.getElementById('agent-modal').classList.add('active');
}
function updateModalIfOpen(agent) {
if (currentModalAgentId === agent.id) {
openModal(agent.id);
}
}
function closeModal() {
document.getElementById('agent-modal').classList.remove('active');
// Stop all videos
document.querySelectorAll('#modal-videos video').forEach(v => v.pause());
// Exit selection mode when closing modal
if (selectionMode) {
selectionMode = false;
selectedImages.clear();
selectedVideos.clear();
document.getElementById('bulk-action-bar').classList.remove('active');
}
// Hide fix panel
hideFixPanel();
currentModalAgentId = null;
}
function generateMedia(type) {
if (!currentModalAgentId || !ws || ws.readyState !== WebSocket.OPEN) {
showError('Not connected');
return;
}
ws.send(JSON.stringify({
type: 'generate_media',
payload: {
agent_id: currentModalAgentId,
media_type: type
}
}));
showToast(`Generating ${type}...`);
}
function deleteImage(imageUrl) {
if (!currentModalAgentId || !ws || ws.readyState !== WebSocket.OPEN) {
showError('Not connected');
return;
}
if (!confirm('Delete this image?')) return;
ws.send(JSON.stringify({
type: 'delete_image',
payload: {
agent_id: currentModalAgentId,
image_url: imageUrl
}
}));
}
function deleteVideo(videoUrl) {
if (!currentModalAgentId || !ws || ws.readyState !== WebSocket.OPEN) {
showError('Not connected');
return;
}
if (!confirm('Delete this video?')) return;
ws.send(JSON.stringify({
type: 'delete_video',
payload: {
agent_id: currentModalAgentId,
video_url: videoUrl
}
}));
}
// Fullscreen gallery viewer
let fullscreenImages = [];
let fullscreenIndex = 0;
function openFullscreen(index) {
const agent = agents[currentModalAgentId];
if (!agent || !agent.images) return;
fullscreenImages = agent.images;
fullscreenIndex = index;
const viewer = document.getElementById('fullscreen-viewer');
updateFullscreenImage();
viewer.classList.add('active');
}
function closeFullscreen() {
document.getElementById('fullscreen-viewer').classList.remove('active');
}
function updateFullscreenImage() {
const img = document.getElementById('fullscreen-image');
const counter = document.getElementById('fullscreen-counter');
img.src = fullscreenImages[fullscreenIndex];
counter.textContent = `${fullscreenIndex + 1} / ${fullscreenImages.length}`;
}
function navigateFullscreen(direction) {
fullscreenIndex += direction;
if (fullscreenIndex < 0) fullscreenIndex = fullscreenImages.length - 1;
if (fullscreenIndex >= fullscreenImages.length) fullscreenIndex = 0;
updateFullscreenImage();
}
document.addEventListener('keydown', (e) => {
const viewer = document.getElementById('fullscreen-viewer');
if (viewer.classList.contains('active')) {
if (e.key === 'Escape') closeFullscreen();
if (e.key === 'ArrowLeft') navigateFullscreen(-1);
if (e.key === 'ArrowRight') navigateFullscreen(1);
return;
}
// Exit selection mode with Escape
if (e.key === 'Escape' && selectionMode) {
exitSelectionMode();
}
});
// Selection mode functions
function toggleSelectionMode() {
selectionMode = !selectionMode;
updateSelectionModeUI();
}
function exitSelectionMode() {
selectionMode = false;
selectedImages.clear();
selectedVideos.clear();
updateSelectionModeUI();
// Re-render to clear checkbox states
if (currentModalAgentId) {
openModal(currentModalAgentId);
}
}
function updateSelectionModeUI() {
const modalContent = document.querySelector('.modal-content');
const bulkBar = document.getElementById('bulk-action-bar');
const selectBtn = document.getElementById('select-multiple-btn');
if (selectionMode) {
modalContent.classList.add('selection-mode');
bulkBar.classList.add('active');
selectBtn.textContent = 'Cancel Selection';
selectBtn.style.borderColor = '#ff6b9d';
selectBtn.style.color = '#ff6b9d';
} else {
modalContent.classList.remove('selection-mode');
bulkBar.classList.remove('active');
selectBtn.textContent = 'Select Multiple';
selectBtn.style.borderColor = '';
selectBtn.style.color = '';
}
updateBulkCount();
}
function toggleImageSelection(url, event) {
event.stopPropagation();
if (selectedImages.has(url)) {
selectedImages.delete(url);
} else {
selectedImages.add(url);
}
// Update checkbox visual
const checkbox = event.target;
checkbox.classList.toggle('selected', selectedImages.has(url));
updateBulkCount();
}
function toggleVideoSelection(url, event) {
event.stopPropagation();
if (selectedVideos.has(url)) {
selectedVideos.delete(url);
} else {
selectedVideos.add(url);
}
// Update checkbox visual
const checkbox = event.target;
checkbox.classList.toggle('selected', selectedVideos.has(url));
updateBulkCount();
}
function handleGalleryClick(index, url, event) {
if (selectionMode) {
toggleImageSelection(url, event);
} else {
openFullscreen(index);
}
}
function handleVideoClick(event) {
// In selection mode, don't play video on click
if (selectionMode) {
event.preventDefault();
event.stopPropagation();
}
}
function updateBulkCount() {
const total = selectedImages.size + selectedVideos.size;
document.getElementById('bulk-count').textContent = total;
document.getElementById('bulk-delete-btn').disabled = total === 0;
}
function bulkDelete() {
const total = selectedImages.size + selectedVideos.size;
if (total === 0) return;
if (!confirm(`Delete ${total} selected item${total > 1 ? 's' : ''}? This cannot be undone.`)) {
return;
}
if (!currentModalAgentId || !ws || ws.readyState !== WebSocket.OPEN) {
showError('Not connected');
return;
}
ws.send(JSON.stringify({
type: 'bulk_delete',
payload: {
agent_id: currentModalAgentId,
image_urls: Array.from(selectedImages),
video_urls: Array.from(selectedVideos)
}
}));
}
// Fix Panel Functions
function toggleFixPanel() {
const panel = document.getElementById('fix-panel');
const btn = document.getElementById('fix-issues-btn');
const isActive = panel.classList.toggle('active');
btn.style.borderColor = isActive ? '#ff6b9d' : '';
btn.style.color = isActive ? '#ff6b9d' : '';
}
function hideFixPanel() {
const panel = document.getElementById('fix-panel');
const btn = document.getElementById('fix-issues-btn');
panel.classList.remove('active');
btn.style.borderColor = '';
btn.style.color = '';
}
function regeneratePositions() {
if (!currentModalAgentId || !ws || ws.readyState !== WebSocket.OPEN) {
showError('Not connected');
return;
}
const input = document.getElementById('fix-positions').value.trim();
if (!input) {
showError('Please enter position numbers');
return;
}
// Parse comma-separated positions
const positions = input.split(',')
.map(s => parseInt(s.trim(), 10))
.filter(n => !isNaN(n));
if (positions.length === 0) {
showError('Please enter valid position numbers');
return;
}
// Validate range
const invalid = positions.filter(p => p < 1 || p > 75);
if (invalid.length > 0) {
showError(`Invalid positions: ${invalid.join(', ')}. Must be between 1 and 75.`);
return;
}
ws.send(JSON.stringify({
type: 'regenerate_positions',
payload: {
agent_id: currentModalAgentId,
positions: positions
}
}));
// Clear input after sending
document.getElementById('fix-positions').value = '';
}
function modifyPersona(modification, intensity = '') {
if (!currentModalAgentId || !ws || ws.readyState !== WebSocket.OPEN) {
showError('Not connected');
return;
}
ws.send(JSON.stringify({
type: 'modify_persona',
payload: {
agent_id: currentModalAgentId,
modification: modification,
intensity: intensity
}
}));
}
function regenerateBanner() {
if (!currentModalAgentId || !ws || ws.readyState !== WebSocket.OPEN) {
showError('Not connected');
return;
}
const style = document.getElementById('banner-style').value;
ws.send(JSON.stringify({
type: 'regenerate_banner',
payload: {
agent_id: currentModalAgentId,
style: style
}
}));
}
</script>
</body>
</html>