- Add image intake service with format validation (JPEG, PNG, WebP) and EXIF/GPS stripping - Enforce strict single-image rule across Web and Signal attachment channels - Implement token-optimized vision downscaling and JPEG compression - Add IMAGE_CONTEXT pipeline stage with OmniRoute vision routing and resilient failover - Seed and manage versioned idea-image-interpreter prompt in catalog - Update Web UI with responsive image picker, preview chip, and Visual Context tab - Add comprehensive automated test suite in test_image_intake.py - Update README and Labyricorn devlog
447 lines
25 KiB
HTML
447 lines
25 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Admin Control Center - ThinkStorm{% endblock %}
|
|
|
|
{% block content %}
|
|
<div style="margin-bottom: 2rem;">
|
|
<div style="display:flex; align-items:center; gap:0.6rem; margin-bottom:0.4rem;">
|
|
<span class="badge badge-active" style="font-size:0.8rem;">Administrator</span>
|
|
</div>
|
|
<h1 style="font-size: 2.2rem; margin-bottom: 0.5rem;">Orchestrator Command Center</h1>
|
|
<p style="color: var(--text-secondary);">Manage prompts, immutable versions, external service adapters, queues, moderation, and token metrics.</p>
|
|
</div>
|
|
|
|
<div class="tab-container glass-card" style="padding:1.75rem;">
|
|
<div class="tab-nav">
|
|
<button class="tab-btn active" data-tab="adm-prompts">📝 Prompts & Versions</button>
|
|
<button class="tab-btn" data-tab="adm-profiles">🧩 Prompt Profiles</button>
|
|
<button class="tab-btn" data-tab="adm-services">🔌 Service Adapters</button>
|
|
<button class="tab-btn" data-tab="adm-jobs">⚡ Job Queue & Provenance</button>
|
|
<button class="tab-btn" data-tab="adm-quarantine">🛡️ Quarantine & Moderation</button>
|
|
<button class="tab-btn" data-tab="adm-trash">🗑️ Trash Bin ({{ trashed_count }})</button>
|
|
<button class="tab-btn" data-tab="adm-metrics">📊 Token Accounting & Audits</button>
|
|
</div>
|
|
|
|
<!-- 1. Prompts Management -->
|
|
<div id="adm-prompts" class="tab-panel active">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.25rem;">
|
|
<div>
|
|
<h3 style="font-size:1.15rem;">Prompt Catalog & Immutable Versioning</h3>
|
|
<p style="color:var(--text-secondary); font-size:0.88rem;">Editing a prompt creates a new version record. Historical runs remain immutably tied to their original version.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex; flex-direction:column; gap:1rem;">
|
|
{% for p in prompts %}
|
|
<div style="background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1.25rem;">
|
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.75rem; flex-wrap:wrap; gap:0.5rem;">
|
|
<div>
|
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
|
<span style="font-family:var(--font-mono); font-weight:700; color:var(--accent-primary);">{{ p.id }}</span>
|
|
<span class="badge" style="background:rgba(99,102,241,0.15); color:#a5b4fc;">v{{ p.current_version }}</span>
|
|
<span class="badge" style="background:rgba(255,255,255,0.06);">{{ p.stage }}</span>
|
|
<span class="badge" style="background:rgba(16,185,129,0.1); color:#34d399;">Policy: {{ p.model_policy }}</span>
|
|
</div>
|
|
<h4 style="font-size:1.1rem; margin-top:0.3rem;">{{ p.name }}</h4>
|
|
<p style="color:var(--text-secondary); font-size:0.85rem;">{{ p.description }}</p>
|
|
</div>
|
|
|
|
<div style="display:flex; gap:0.5rem;">
|
|
<button onclick="openPromptEditor('{{ p.id }}')" class="btn btn-secondary btn-sm">✏️ Edit (New Version)</button>
|
|
<button onclick="togglePrompt('{{ p.id }}')" class="btn btn-secondary btn-sm">{% if p.enabled %}Disable{% else %}Enable{% endif %}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<details style="margin-top:0.75rem;">
|
|
<summary style="cursor:pointer; font-size:0.82rem; color:var(--text-muted); font-family:var(--font-mono);">
|
|
View Current Prompts & Hash ({{ p.prompt_hash[:20] }}...)
|
|
</summary>
|
|
<div style="margin-top:0.75rem; display:grid; grid-template-columns:1fr 1fr; gap:1rem; font-size:0.85rem;">
|
|
<div>
|
|
<strong style="color:var(--text-secondary);">System Prompt:</strong>
|
|
<pre style="margin-top:0.3rem; white-space:pre-wrap; max-height:220px; overflow-y:auto;">{{ p.system_prompt }}</pre>
|
|
</div>
|
|
<div>
|
|
<strong style="color:var(--text-secondary);">User Prompt Template:</strong>
|
|
<pre style="margin-top:0.3rem; white-space:pre-wrap; max-height:220px; overflow-y:auto;">{{ p.user_prompt_template }}</pre>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. Profiles -->
|
|
<div id="adm-profiles" class="tab-panel">
|
|
<h3 style="font-size:1.15rem; margin-bottom:0.5rem;">Prompt Profiles</h3>
|
|
<p style="color:var(--text-secondary); font-size:0.88rem; margin-bottom:1.25rem;">Prompt profiles group stage-specific prompts for different idea archetypes.</p>
|
|
|
|
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:1.25rem;">
|
|
{% for prof in profiles %}
|
|
<div style="background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1.25rem;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
|
|
<h4 style="font-size:1.1rem; color:var(--accent-primary);">{{ prof.name }}</h4>
|
|
{% if prof.is_default %}<span class="badge badge-success">Default</span>{% endif %}
|
|
</div>
|
|
<p style="color:var(--text-secondary); font-size:0.85rem; margin-bottom:1rem;">{{ prof.description }}</p>
|
|
<div style="font-family:var(--font-mono); font-size:0.8rem; background:rgba(0,0,0,0.4); padding:0.75rem; border-radius:var(--radius-sm);">
|
|
{% for stage, p_str in prof.prompt_assignments.items() %}
|
|
<div style="display:flex; justify-content:space-between; margin-bottom:0.25rem;">
|
|
<span style="color:var(--text-muted);">{{ stage }}:</span>
|
|
<span style="color:var(--text-primary);">{{ p_str }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. Services -->
|
|
<div id="adm-services" class="tab-panel">
|
|
<h3 style="font-size:1.15rem; margin-bottom:0.5rem;">External Service Adapters</h3>
|
|
<p style="color:var(--text-secondary); font-size:0.88rem; margin-bottom:1.25rem;">Live health diagnostics and endpoints configuration for orchestrated services.</p>
|
|
|
|
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(320px, 1fr)); gap:1.25rem;">
|
|
{% for s in services %}
|
|
<div style="background:rgba(0,0,0,0.3); border:1px solid var(--border-glass); border-radius:var(--radius-md); padding:1.25rem;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem;">
|
|
<h4 style="font-size:1.1rem;">{{ s.name }}</h4>
|
|
<span class="badge {% if s.health_status == 'HEALTHY' %}badge-available{% else %}badge-claimed{% endif %}">
|
|
{{ s.health_status }}
|
|
</span>
|
|
</div>
|
|
|
|
<div style="font-size:0.85rem; color:var(--text-secondary); margin-bottom:0.5rem; word-break:break-all;">
|
|
<strong>Endpoint:</strong> <span style="font-family:var(--font-mono);">{{ s.endpoint }}</span>
|
|
</div>
|
|
|
|
{% if s.api_key_masked %}
|
|
<div style="font-size:0.85rem; color:var(--text-secondary); margin-bottom:0.75rem;">
|
|
<strong>API Key:</strong> <span style="font-family:var(--font-mono);">{{ s.api_key_masked }}</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if s.last_tested_at %}
|
|
<div style="font-size:0.75rem; color:var(--text-muted); margin-bottom:1rem;">
|
|
Last Checked: {{ s.last_tested_at[:19] }}Z
|
|
</div>
|
|
{% endif %}
|
|
|
|
<button id="test-btn-{{ s.id }}" onclick="testService('{{ s.id }}')" class="btn btn-secondary btn-sm" style="width:100%;">
|
|
⚡ Test Connection
|
|
</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 4. Job Observability -->
|
|
<div id="adm-jobs" class="tab-panel">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.25rem;">
|
|
<h3 style="font-size:1.15rem;">Job Queue & Provenance Observability</h3>
|
|
<div style="display:flex; gap:1rem; font-family:var(--font-mono); font-size:0.85rem;">
|
|
<span>Foreground: <strong>{{ jobs_data.queue.foreground_queued }}</strong></span>
|
|
<span>Background: <strong>{{ jobs_data.queue.background_queued }}</strong></span>
|
|
<span>Active: <strong>{{ jobs_data.queue.active_jobs_count }}</strong></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="overflow-x:auto;">
|
|
<table style="width:100%; border-collapse:collapse; font-size:0.85rem; text-align:left;">
|
|
<thead>
|
|
<tr style="border-bottom:1px solid var(--border-glass); color:var(--text-muted);">
|
|
<th style="padding:0.6rem;">Run ID</th>
|
|
<th style="padding:0.6rem;">Idea ID</th>
|
|
<th style="padding:0.6rem;">Processor</th>
|
|
<th style="padding:0.6rem;">Model</th>
|
|
<th style="padding:0.6rem;">Tokens</th>
|
|
<th style="padding:0.6rem;">Duration</th>
|
|
<th style="padding:0.6rem;">Status</th>
|
|
<th style="padding:0.6rem;">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in jobs_data.recent_runs %}
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.04);">
|
|
<td style="padding:0.6rem; font-family:var(--font-mono); color:var(--accent-primary);">{{ r.id }}</td>
|
|
<td style="padding:0.6rem; font-family:var(--font-mono);"><a href="/ideas/{{ r.idea_id }}">{{ r.idea_id }}</a></td>
|
|
<td style="padding:0.6rem;"><strong>{{ r.processor_name }}</strong></td>
|
|
<td style="padding:0.6rem; font-family:var(--font-mono); font-size:0.78rem;">{{ r.resolved_model }}</td>
|
|
<td style="padding:0.6rem; font-family:var(--font-mono);">{{ r.total_tokens }}</td>
|
|
<td style="padding:0.6rem; font-family:var(--font-mono);">{{ r.duration_ms }}ms</td>
|
|
<td style="padding:0.6rem;"><span class="badge badge-success">{{ r.status }}</span></td>
|
|
<td style="padding:0.6rem;">
|
|
<button onclick="retryJob('{{ r.idea_id }}')" class="btn btn-secondary btn-sm" style="padding:0.2rem 0.5rem; font-size:0.75rem;">Retry Idea</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 5. Quarantine & Moderation -->
|
|
<div id="adm-quarantine" class="tab-panel">
|
|
<h3 style="font-size:1.15rem; margin-bottom:0.5rem;">Quarantined Submissions & Moderation</h3>
|
|
<p style="color:var(--text-secondary); font-size:0.88rem; margin-bottom:1.25rem;">Submissions flagged with malicious or suspicious URLs are held for administrative review before retrieval.</p>
|
|
|
|
{% if quarantined and quarantined|length > 0 %}
|
|
<div style="display:flex; flex-direction:column; gap:1.25rem;">
|
|
{% for q in quarantined %}
|
|
<div style="background:rgba(239,68,68,0.05); border:1px solid rgba(239,68,68,0.25); border-radius:var(--radius-md); padding:1.25rem;">
|
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.75rem;">
|
|
<div>
|
|
<span style="font-family:var(--font-mono); font-weight:700; color:var(--color-danger);">{{ q.id }}</span>
|
|
<span class="badge badge-quarantined" style="margin-left:0.5rem;">QUARANTINED</span>
|
|
<div style="font-size:0.85rem; color:var(--text-muted); margin-top:0.25rem;">Submitted: {{ q.submitted_at[:19] }}Z</div>
|
|
</div>
|
|
|
|
<div style="display:flex; gap:0.5rem;">
|
|
<button onclick="quarantineDecision('{{ q.id }}', 'APPROVE')" class="btn btn-success btn-sm">Approve & Release</button>
|
|
<button onclick="quarantineDecision('{{ q.id }}', 'REJECT')" class="btn btn-danger btn-sm">Reject Submission</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="markdown-view-container" data-markdown-view style="margin-bottom:0.75rem;">
|
|
<div class="markdown-view-header" style="margin-bottom:0.4rem;">
|
|
<div class="markdown-view-actions">
|
|
<div class="markdown-toggle-pill">
|
|
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
|
|
<span>👁️ Formatted</span>
|
|
</button>
|
|
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
|
|
<span>💻 Code</span>
|
|
</button>
|
|
</div>
|
|
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy text">
|
|
<span>📋 Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="markdown-view-body" style="background:rgba(0,0,0,0.3); padding:0.75rem; border-radius:var(--radius-sm); border:1px solid var(--border-glass); font-size:0.9rem;">
|
|
<div class="markdown-formatted markdown-body"></div>
|
|
<div class="markdown-code" style="display:none; background:transparent; border:none; padding:0; margin:0;">
|
|
<pre style="margin:0; background:transparent; border:none; padding:0; line-height:1.6;"><code>{{ q.original_text }}</code></pre>
|
|
</div>
|
|
<textarea class="raw-markdown-source" style="display:none;">{{ q.original_text }}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h5 style="font-size:0.85rem; color:var(--text-secondary); margin-bottom:0.4rem;">Flagged URLs:</h5>
|
|
{% for u in q.urls %}
|
|
<div style="font-family:var(--font-mono); font-size:0.8rem; background:rgba(0,0,0,0.4); padding:0.4rem 0.6rem; border-radius:var(--radius-sm); margin-bottom:0.3rem; display:flex; justify-content:space-between;">
|
|
<span>{{ u.url }}</span>
|
|
<span class="badge badge-malicious">{{ u.safety_state }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p style="color:var(--text-muted);">No quarantined ideas requiring review.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- 6. Trash Bin Management -->
|
|
<div id="adm-trash" class="tab-panel">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.25rem; flex-wrap:wrap; gap:1rem;">
|
|
<div>
|
|
<h3 style="font-size:1.15rem; color:#f87171;">Trash Bin & Idea Retention</h3>
|
|
<p style="color:var(--text-secondary); font-size:0.88rem;">Items moved to trash are withheld from incubation until restored or permanently emptied.</p>
|
|
</div>
|
|
{% if trashed_ideas and trashed_ideas|length > 0 %}
|
|
<button onclick="emptyTrash()" class="btn btn-danger" style="display:flex; align-items:center; gap:0.4rem;">
|
|
<span>🗑️ Empty Trash ({{ trashed_ideas|length }})</span>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if trashed_ideas and trashed_ideas|length > 0 %}
|
|
<div style="display:flex; flex-direction:column; gap:1rem;">
|
|
{% for t in trashed_ideas %}
|
|
<div style="background:rgba(0,0,0,0.3); border:1px solid rgba(239,68,68,0.25); border-radius:var(--radius-md); padding:1.25rem;">
|
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.75rem; flex-wrap:wrap; gap:0.5rem;">
|
|
<div>
|
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
|
<a href="/ideas/{{ t.id }}" style="font-family:var(--font-mono); font-weight:700; color:var(--accent-primary);">{{ t.id }}</a>
|
|
<span class="badge badge-trashed">TRASHED</span>
|
|
{% if t.previous_lifecycle_state %}
|
|
<span class="badge" style="background:rgba(255,255,255,0.06); font-size:0.7rem;">Prev: {{ t.previous_lifecycle_state }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<h4 style="font-size:1.05rem; margin-top:0.3rem;">{{ t.title or "Untitled Idea" }}</h4>
|
|
<div style="font-size:0.8rem; color:var(--text-muted); margin-top:0.2rem; font-family:var(--font-mono);">
|
|
Submitted: {{ t.submitted_at[:19] }}Z {% if t.trashed_at %}• Trashed: {{ t.trashed_at[:19] }}Z{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex; gap:0.5rem;">
|
|
<button onclick="restoreIdea('{{ t.id }}')" class="btn btn-secondary btn-sm" style="color:#34d399;">♻️ Restore</button>
|
|
<button onclick="deleteIdeaPermanently('{{ t.id }}')" class="btn btn-danger btn-sm">❌ Delete Forever</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="markdown-view-container" data-markdown-view>
|
|
<div class="markdown-view-header" style="margin-bottom:0.4rem;">
|
|
<div class="markdown-view-actions">
|
|
<div class="markdown-toggle-pill">
|
|
<button type="button" class="btn-toggle-view active" data-view="formatted" title="Formatted View">
|
|
<span>👁️ Formatted</span>
|
|
</button>
|
|
<button type="button" class="btn-toggle-view" data-view="code" title="Code / Raw View">
|
|
<span>💻 Code</span>
|
|
</button>
|
|
</div>
|
|
<button type="button" class="btn-copy-markdown" onclick="copyMarkdownFromContainer(this)" title="Copy text">
|
|
<span>📋 Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="markdown-view-body" style="background:rgba(0,0,0,0.3); padding:0.75rem; border-radius:var(--radius-sm); border:1px solid var(--border-glass); font-size:0.88rem; color:var(--text-secondary);">
|
|
<div class="markdown-formatted markdown-body"></div>
|
|
<div class="markdown-code" style="display:none; background:transparent; border:none; padding:0; margin:0;">
|
|
<pre style="margin:0; background:transparent; border:none; padding:0; line-height:1.6;"><code>{{ t.original_text }}</code></pre>
|
|
</div>
|
|
<textarea class="raw-markdown-source" style="display:none;">{{ t.original_text }}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div style="text-align:center; padding:2.5rem; background:rgba(0,0,0,0.2); border-radius:var(--radius-md); border:1px solid var(--border-glass);">
|
|
<p style="color:var(--text-muted); margin:0;">Trash is empty. No ideas are currently in the trash bin.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- 7. Metrics & Audits -->
|
|
<div id="adm-metrics" class="tab-panel">
|
|
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:1.25rem; margin-bottom:2rem;">
|
|
<div style="background:rgba(0,0,0,0.3); padding:1.25rem; border-radius:var(--radius-md); border:1px solid var(--border-glass);">
|
|
<div style="font-size:0.85rem; color:var(--text-secondary);">Total Tokens</div>
|
|
<div style="font-size:1.8rem; font-weight:700; font-family:var(--font-mono); color:var(--accent-primary);">{{ metrics.overall.total_tokens }}</div>
|
|
</div>
|
|
<div style="background:rgba(0,0,0,0.3); padding:1.25rem; border-radius:var(--radius-md); border:1px solid var(--border-glass);">
|
|
<div style="font-size:0.85rem; color:var(--text-secondary);">Input Tokens</div>
|
|
<div style="font-size:1.8rem; font-weight:700; font-family:var(--font-mono);">{{ metrics.overall.input_tokens }}</div>
|
|
</div>
|
|
<div style="background:rgba(0,0,0,0.3); padding:1.25rem; border-radius:var(--radius-md); border:1px solid var(--border-glass);">
|
|
<div style="font-size:0.85rem; color:var(--text-secondary);">Output Tokens</div>
|
|
<div style="font-size:1.8rem; font-weight:700; font-family:var(--font-mono);">{{ metrics.overall.output_tokens }}</div>
|
|
</div>
|
|
<div style="background:rgba(0,0,0,0.3); padding:1.25rem; border-radius:var(--radius-md); border:1px solid var(--border-glass);">
|
|
<div style="font-size:0.85rem; color:var(--text-secondary);">Processor Runs</div>
|
|
<div style="font-size:1.8rem; font-weight:700; font-family:var(--font-mono);">{{ metrics.overall.runs_count }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h4 style="font-size:1.1rem; margin-bottom:0.75rem;">Recent Administrative Audit Log</h4>
|
|
<div style="overflow-x:auto;">
|
|
<table style="width:100%; border-collapse:collapse; font-size:0.82rem; text-align:left;">
|
|
<thead>
|
|
<tr style="border-bottom:1px solid var(--border-glass); color:var(--text-muted);">
|
|
<th style="padding:0.5rem;">Timestamp</th>
|
|
<th style="padding:0.5rem;">User</th>
|
|
<th style="padding:0.5rem;">Action</th>
|
|
<th style="padding:0.5rem;">Entity</th>
|
|
<th style="padding:0.5rem;">Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for a in audit_logs %}
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.04);">
|
|
<td style="padding:0.5rem; font-family:var(--font-mono); color:var(--text-muted);">{{ a.created_at[:19] }}Z</td>
|
|
<td style="padding:0.5rem;"><strong>{{ a.user_id }}</strong></td>
|
|
<td style="padding:0.5rem;"><span class="badge badge-claimed">{{ a.action }}</span></td>
|
|
<td style="padding:0.5rem; font-family:var(--font-mono);">{{ a.entity_type }}:{{ a.entity_id }}</td>
|
|
<td style="padding:0.5rem; font-family:var(--font-mono); font-size:0.75rem;">{{ a.details }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal for Prompt Editor -->
|
|
<div id="prompt-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.8); backdrop-filter:blur(8px); z-index:200; align-items:center; justify-content:center; padding:1.5rem;">
|
|
<div class="glass-card" style="width:100%; max-width:700px; padding:2rem; max-height:90vh; overflow-y:auto;">
|
|
<h3 id="modal-prompt-title" style="font-size:1.3rem; margin-bottom:0.5rem;">Edit Prompt</h3>
|
|
<p style="color:var(--text-muted); font-size:0.85rem; margin-bottom:1.25rem;">Saving creates a new immutable version without breaking historical execution provenance.</p>
|
|
|
|
<form id="prompt-edit-form">
|
|
<input type="hidden" id="edit-prompt-id">
|
|
<div class="form-group">
|
|
<label class="form-label">System Prompt</label>
|
|
<textarea id="edit-system-prompt" class="form-textarea" rows="6" required></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">User Prompt Template (use {{submission_text}} or {{title}})</label>
|
|
<textarea id="edit-user-prompt" class="form-textarea" rows="6" required></textarea>
|
|
</div>
|
|
<div style="display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.5rem;">
|
|
<button type="button" onclick="closePromptEditor()" class="btn btn-secondary">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save New Version</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function openPromptEditor(promptId) {
|
|
const res = await fetch(`/api/admin/prompts/${promptId}`);
|
|
const data = await res.json();
|
|
document.getElementById('edit-prompt-id').value = data.id;
|
|
document.getElementById('modal-prompt-title').innerText = `Edit: ${data.name} (Current: v${data.version})`;
|
|
document.getElementById('edit-system-prompt').value = data.system_prompt;
|
|
document.getElementById('edit-user-prompt').value = data.user_prompt_template;
|
|
document.getElementById('prompt-modal').style.display = 'flex';
|
|
}
|
|
|
|
function closePromptEditor() {
|
|
document.getElementById('prompt-modal').style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('prompt-edit-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const promptId = document.getElementById('edit-prompt-id').value;
|
|
const systemPrompt = document.getElementById('edit-system-prompt').value;
|
|
const userPrompt = document.getElementById('edit-user-prompt').value;
|
|
|
|
try {
|
|
const res = await fetch(`/api/admin/prompts/${promptId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
system_prompt: systemPrompt,
|
|
user_prompt_template: userPrompt
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Failed to update prompt');
|
|
showToast(data.message, 'success');
|
|
closePromptEditor();
|
|
setTimeout(() => window.location.reload(), 800);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
});
|
|
|
|
async function togglePrompt(promptId) {
|
|
try {
|
|
const res = await fetch(`/api/admin/prompts/${promptId}/toggle`, { method: 'POST' });
|
|
const data = await res.json();
|
|
showToast(data.message, 'info');
|
|
setTimeout(() => window.location.reload(), 600);
|
|
} catch (err) {
|
|
showToast(err.message, 'danger');
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|