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>
1264 lines
55 KiB
HTML
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()">×</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()">×</button>
|
|
<button class="fullscreen-nav fullscreen-prev" onclick="navigateFullscreen(-1)">←</button>
|
|
<img class="fullscreen-image" id="fullscreen-image" src="" alt="Full size">
|
|
<button class="fullscreen-nav fullscreen-next" onclick="navigateFullscreen(1)">→</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 = '✓';
|
|
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">×</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">×</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>
|