booth-musicgpt/templates/admin/request.html

775 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Request #{{ req.id }} — Admin</title>
<style>
/*
Operator detail page for a single request.
Two-column layout:
left -> customer info + internal operator notes
right -> Suno prompt, song upload, customer notification,
payment reference, and file delivery.
*/
*{box-sizing:border-box;}
body{
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background:#111827;
color:#f3f4f6;
margin:0;
padding:1rem;
line-height:1.5;
}
.container{max-width:1200px;margin:0 auto;}
h1,h2{color:#60a5fa;margin-top:0;}
a{color:#93c5fd;}
.section{
background:#1f2937;
padding:1rem;
border-radius:.5rem;
margin-bottom:1rem;
}
label{display:block;margin-top:.8rem;font-weight:600;}
input,textarea{
width:100%;
padding:.6rem;
border-radius:.5rem;
border:1px solid #374151;
background:#111827;
color:#f3f4f6;
box-sizing:border-box;
font:inherit;
}
textarea{min-height:100px;}
button{
padding:.7rem 1rem;
border:none;
border-radius:.5rem;
background:#3b82f6;
color:#fff;
font-weight:700;
cursor:pointer;
margin-top:.5rem;
}
button.secondary{background:#4b5563;}
button.danger{background:#dc2626;}
button.success{background:#10b981;}
button:disabled{background:#374151;color:#9ca3af;cursor:not-allowed;}
.actions{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem;}
.flash{
padding:.8rem;
background:#064e3b;
border-radius:.5rem;
margin-bottom:1rem;
}
.flash.error{background:#450a0a;}
.status-badge{
display:inline-block;
padding:.25rem .6rem;
border-radius:9999px;
font-size:.8rem;
font-weight:600;
background:#374151;
color:#f3f4f6;
margin-right:.3rem;
}
.status-badge.ok{background:#10b981;color:#000;}
.status-badge.missing{background:#ef4444;color:#000;}
.status-badge.sent{background:#60a5fa;color:#000;}
.copy-hint{font-size:.85rem;color:#9ca3af;margin-top:.3rem;}
.revision-note{
margin-top:1rem;
background:#450a0a;
border:1px solid #7f1d1d;
border-radius:.5rem;
padding:1rem;
}
.revision-note h3{
margin-top:0;
color:#f87171;
}
.file-select{
background:#111827;
padding:.6rem;
border-radius:.5rem;
margin:.3rem 0;
}
.file-select input{
width:auto;
margin-right:.5rem;
}
.file-select label{
display:inline;
margin:0;
font-weight:400;
}
.song-row{
display:flex;
align-items:flex-start;
gap:.75rem;
margin:.6rem 0;
padding:.4rem 0;
}
.song-row input[type="checkbox"]{
width:auto;
margin:.2rem 0 0 0;
flex-shrink:0;
}
.song-row label{
display:block;
margin:0;
font-weight:400;
line-height:1.4;
}
.old-rev{
font-size:.85rem;
color:#9ca3af;
margin-left:1.8rem;
}
.approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24;}
.status-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;}
.media-status{
display:flex;
align-items:flex-start;
gap:1rem;
flex-wrap:wrap;
}
.cover-col{flex-shrink:0;}
.status-col{flex:1;min-width:200px;}
.steps{
display:flex;
list-style:none;
margin:0;
padding:0;
gap:.25rem;
align-items:flex-start;
}
.step-item{
display:flex;
flex-direction:column;
align-items:center;
text-align:center;
flex:1;
min-width:80px;
position:relative;
}
.step-item:not(:last-child)::after{
content:"";
position:absolute;
top:1rem;
left:50%;
width:100%;
height:2px;
background:#4b5563;
z-index:0;
}
.step-item.completed:not(:last-child)::after{
background:#10b981;
}
.step-indicator{
width:2rem;
height:2rem;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
font-size:.9rem;
font-weight:700;
background:#374151;
color:#f3f4f6;
position:relative;
z-index:1;
border:2px solid transparent;
}
.step-item.completed .step-indicator{
background:#10b981;
color:#000;
}
.step-item.current .step-indicator{
background:#3b82f6;
color:#fff;
border-color:#93c5fd;
}
.step-item.upcoming .step-indicator{
background:#374151;
color:#9ca3af;
}
.step-label{
font-size:.75rem;
margin-top:.35rem;
color:#f3f4f6;
}
.step-item.upcoming .step-label{
color:#9ca3af;
}
.history-list{
margin:0;
padding:0;
list-style:none;
}
.history-list li{
border-bottom:1px solid #374151;
padding:.6rem 0;
}
.history-list li:last-child{border-bottom:none;}
.history-list .meta{
font-size:.8rem;
color:#9ca3af;
}
.history-list .note{
margin:.3rem 0 0 0;
}
/* Two-column layout */
.two-col{
display:grid;
grid-template-columns:1fr 1fr;
gap:1rem;
}
@media(max-width:900px){
.two-col{grid-template-columns:1fr;}
}
</style>
</head>
<body>
<div class="container">
<p><a href="{{ url_for('admin_dashboard') }}">← Dashboard</a></p>
<h1>Request #{{ req.id }} — {{ req.name }}</h1>
<!-- Album cover + status info panel -->
<div class="section">
<div class="media-status">
{% if req.album_cover_url %}
<div class="cover-col">
<img src="{{ req.album_cover_url }}" alt="Album cover" style="max-width:160px;border-radius:.5rem;display:block;">
{% if not deliver_album_cover %}
<p class="copy-hint" style="margin:.25rem 0 0 0;max-width:160px;">Album cover is hidden from the customer preview/delivery because delivery is disabled in settings.</p>
{% endif %}
</div>
{% endif %}
<div class="status-col">
<p style="margin:0;font-size:.8rem;color:#9ca3af;text-transform:uppercase;letter-spacing:.05em">Request Status</p>
{% if current_step == -1 %}
<p style="margin:.25rem 0 0 0"><span class="status-badge missing">Cancelled</span></p>
{% else %}
<ol class="steps" aria-label="Request progress">
{% for i in range(steps|length) %}
{% set key, label = steps[i] %}
{% set is_completed = i < current_step %}
{% set is_current = i == current_step %}
<li class="step-item {% if is_completed %}completed{% elif is_current %}current{% else %}upcoming{% endif %}" {% if is_current %}aria-current="step"{% endif %}>
<div class="step-indicator">
{% if is_completed %}✓{% else %}{{ i + 1 }}{% endif %}
</div>
<div class="step-label">{{ label }}</div>
</li>
{% endfor %}
</ol>
{% endif %}
{% if req.musicgpt_status %}
<p style="margin:1rem 0 0 0;font-size:.8rem;color:#9ca3af;text-transform:uppercase;letter-spacing:.05em">Recording Studio Status</p>
<ol class="steps" aria-label="MusicGPT progress">
{% set mg_steps = [
('IN_QUEUE', 'Queued'),
('IN_PROGRESS', 'Generating'),
('COMPLETED', 'Completed')
] %}
{% set current_mg = req.musicgpt_status %}
{% set ns = namespace(mg_index=-1) %}
{% for j in range(mg_steps|length) %}
{% if mg_steps[j][0] == current_mg %}{% set ns.mg_index = j %}{% endif %}
{% endfor %}
{% if current_mg in ('FAILED', 'ERROR', 'CANCELLED') %}{% set ns.mg_index = -2 %}{% endif %}
{% for j in range(mg_steps|length) %}
{% set mg_key, mg_label = mg_steps[j] %}
{% set mg_completed = j < ns.mg_index %}
{% set mg_current = j == ns.mg_index %}
<li class="step-item {% if mg_completed %}completed{% elif mg_current %}current{% else %}upcoming{% endif %} {% if ns.mg_index == -2 and not mg_completed and not mg_current %}upcoming{% endif %}" {% if mg_current %}aria-current="step"{% endif %}>
<div class="step-indicator">
{% if mg_completed %}✓{% else %}{{ j + 1 }}{% endif %}
</div>
<div class="step-label">{{ mg_label }}</div>
</li>
{% endfor %}
</ol>
{% if req.musicgpt_cost is not none %}
<p style="margin:.5rem 0 0 0"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(req.musicgpt_cost) }}</p>
{% endif %}
{% endif %}
{% if req.status != 'cancelled' %}
<form method="POST" onsubmit="return confirm('Are you sure you want to cancel request #{{ req.id }}?')" style="margin:.75rem 0 0 0">
<input type="hidden" name="action" value="cancel_request">
<button type="submit" class="danger" style="margin:0">Cancel Request</button>
</form>
{% endif %}
</div>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
<div class="two-col">
<!-- LEFT COLUMN -->
<div class="left">
<!-- Customer Info -->
<div class="section">
<h2>Customer Info</h2>
<p class="copy-hint">Operators can correct customer details here. Click Save when done.</p>
<form method="POST">
<input type="hidden" name="action" value="update_customer_info">
<label for="customer_email">Email address</label>
<input type="email" id="customer_email" name="email" value="{{ req.email }}" required>
<label for="customer_name">Name / persona</label>
<input type="text" id="customer_name" name="name" value="{{ req.name }}" required>
<label for="customer_pronouns">Pronouns <span style="color:#f87171">*</span></label>
<select id="customer_pronouns" name="pronouns" required>
<option value="" {% if not req.pronouns %}selected{% endif %}>— Select pronouns —</option>
<option value="He/Him/His" {% if req.pronouns == 'He/Him/His' %}selected{% endif %}>He/Him/His</option>
<option value="She/Her/Hers" {% if req.pronouns == 'She/Her/Hers' %}selected{% endif %}>She/Her/Hers</option>
<option value="They/Them/Their" {% if req.pronouns == 'They/Them/Their' %}selected{% endif %}>They/Them/Their</option>
</select>
<label for="customer_hobbies">Hobbies &amp; interests</label>
<textarea id="customer_hobbies" name="hobbies" rows="3">{{ req.hobbies or '' }}</textarea>
<label for="customer_notable_facts">Notable things</label>
<textarea id="customer_notable_facts" name="notable_facts" rows="3">{{ req.notable_facts or '' }}</textarea>
<label for="customer_style_genre_decade">Decade / era <span style="color:#f87171">*</span></label>
<select id="customer_style_genre_decade" name="decade" required>
<option value="" {% if not style_parts.decade %}selected{% endif %}>— Select decade —</option>
{% for d in decades %}
<option value="{{ d }}" {% if style_parts.decade == d %}selected{% endif %}>{{ d }}</option>
{% endfor %}
</select>
<label for="customer_style_genre_basic">Basic style <span style="color:#f87171">*</span></label>
<select id="customer_style_genre_basic" name="basic_style" required>
<option value="" {% if not style_parts.basic_style %}selected{% endif %}>— Select style —</option>
{% for g in genres %}
<option value="{{ g }}" {% if style_parts.basic_style == g %}selected{% endif %}>{{ g }}</option>
{% endfor %}
</select>
<label for="customer_style_genre_additional">Additional style (optional)</label>
<select id="customer_style_genre_additional" name="additional_style">
<option value="" {% if not style_parts.additional_style %}selected{% endif %}>— None —</option>
{% for g in genres %}
<option value="{{ g }}" {% if style_parts.additional_style == g %}selected{% endif %}>{{ g }}</option>
{% endfor %}
</select>
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Stored style: {{ req.style_genre or '—' }}</p>
<label for="customer_vocal_gender">Preferred singer voice / gender</label>
<select id="customer_vocal_gender" name="vocal_gender">
<option value="" {% if not req.vocal_gender %}selected{% endif %}>No preference</option>
<option value="female" {% if req.vocal_gender == 'female' %}selected{% endif %}>Female</option>
<option value="male" {% if req.vocal_gender == 'male' %}selected{% endif %}>Male</option>
<option value="non-binary" {% if req.vocal_gender == 'non-binary' %}selected{% endif %}>Non-binary / Gender-neutral</option>
<option value="androgynous" {% if req.vocal_gender == 'androgynous' %}selected{% endif %}>Androgynous</option>
<option value="other" {% if req.vocal_gender == 'other' %}selected{% endif %}>Other (mention in Extra Requests)</option>
</select>
<label for="customer_extra_requests">Anything else</label>
<textarea id="customer_extra_requests" name="extra_requests" rows="3" maxlength="2000">{{ req.extra_requests or '' }}</textarea>
<label for="customer_stems_interest" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
<input type="checkbox" id="customer_stems_interest" name="stems_interest" value="1" {% if req.stems_interest %}checked{% endif %} style="width:auto;margin:0">
Customer is interested in STEMS / multitrack files
</label>
<div class="actions">
<button type="submit" class="secondary">Save Customer Info</button>
</div>
</form>
<p style="margin-top:1rem"><strong>Request #:</strong> {{ req.id }}</p>
{% if req.revision_note %}
<div class="revision-note">
<h3>📝 Revisions Requested{% if req.revision_count %}<span style="float:right">Revision #{{ req.revision_count }}</span>{% endif %}</h3>
<p>{{ req.revision_note }}</p>
<button type="button" onclick="copyRevisionPromptForHermes()">📋 Copy revision prompt for Hermes</button>
</div>
{% endif %}
</div>
<!-- Revision History -->
<div class="section">
<h2>Revision History</h2>
{% if revision_history %}
<ul class="history-list">
{% for h in revision_history %}
<li>
<div class="meta">Revision #{{ h.revision_count }} — {{ h.created_at }}
{% if h.old_song_a_path or h.old_song_b_path %}
<br>Archived:
{% if h.old_song_a_path %}<br>A: {{ basename(h.old_song_a_path) }}{% endif %}
{% if h.old_song_b_path %}<br>B: {{ basename(h.old_song_b_path) }}{% endif %}
{% endif %}
</div>
<p class="note">{{ h.note or 'No note provided.' }}</p>
</li>
{% endfor %}
</ul>
{% else %}
<p class="copy-hint">No revisions recorded yet.</p>
{% endif %}
</div>
<!-- Operator Notes -->
<div class="section">
<h2>Operator Notes</h2>
<p class="copy-hint">Internal notes for the booth team. Customers cannot see this.</p>
<form method="POST">
<input type="hidden" name="action" value="save_operator_notes">
<label for="operator_notes">Notes</label>
<textarea id="operator_notes" name="operator_notes" rows="4" placeholder="e.g. paid cash, VIP friend, follow up...">{{ req.operator_notes or '' }}</textarea>
<div class="actions">
<button type="submit" class="secondary">Save Notes</button>
</div>
</form>
</div>
<!-- Stems / Extras Link -->
<div class="section">
<h2>Stems / Extras Link</h2>
<p class="copy-hint">Paste a self-hosted file share link (e.g. from Pingvin Share). It will appear on the customer player page once delivered.</p>
<form method="POST">
<input type="hidden" name="action" value="save_stems_link">
<label for="stems_link">Share link</label>
<input type="url" id="stems_link" name="stems_link" value="{{ req.stems_link or '' }}" placeholder="https://files.dionysismedia.ca/share/...">
<div class="actions">
<button type="submit" class="secondary">Save Link</button>
</div>
</form>
</div>
</div>
<!-- RIGHT COLUMN -->
<div class="right">
<!-- Generate Music Prompt -->
<div class="section">
<h2>1. Generate & Save Music Prompt</h2>
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save or generate.</p>
{% if req.musicgpt_error %}
<div class="flash error">Error: {{ req.musicgpt_error }}</div>
{% endif %}
<form method="POST">
<input type="hidden" name="action" value="save_prompt">
<label for="suno_title">Title</label>
<input type="text" id="suno_title" name="suno_title" value="{{ req.suno_title or '' }}">
<label for="suno_style">Style</label>
<textarea id="suno_style" name="suno_style">{{ req.suno_style or '' }}</textarea>
<label for="suno_lyrics">Lyrics (with metatags)</label>
<textarea id="suno_lyrics" name="suno_lyrics" rows="10">{{ req.suno_lyrics or '' }}</textarea>
<div class="actions">
<button type="button" class="secondary" onclick="copyToClipboard('suno_lyrics', 'Lyrics')">Copy Lyrics</button>
<button type="button" class="secondary" onclick="copyToClipboard('suno_style', 'Style')">Copy Style</button>
<button type="button" class="secondary" onclick="copyToClipboard('suno_title', 'Title')">Copy Title</button>
<button type="submit">Save Prompt</button>
</div>
</form>
<hr style="border-color:#374151;margin:1rem 0">
<form method="POST">
<input type="hidden" name="action" value="generate_musicgpt">
<input type="hidden" name="suno_title" value="{{ req.suno_title or '' }}">
<input type="hidden" name="suno_style" value="{{ req.suno_style or '' }}">
<input type="hidden" name="suno_lyrics" value="{{ req.suno_lyrics or '' }}">
<label for="musicgpt_model">MusicGPT Model</label>
<select id="musicgpt_model" name="musicgpt_model">
{% for m in musicgpt_models %}
<option value="{{ m }}" {% if m == musicgpt_default_model %}selected{% endif %}>{{ m }}</option>
{% endfor %}
</select>
<label for="deliver_wav_gen" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
<input type="checkbox" id="deliver_wav_gen" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %} style="width:auto;margin:0">
Include WAV files with delivery
</label>
{% set in_progress = req.musicgpt_status in ('IN_QUEUE', 'IN_PROGRESS') %}
<div class="actions">
<button type="submit" {% if in_progress %}disabled{% endif %}>Generate A/B with MusicGPT</button>
{% if in_progress %}
<button type="submit" name="action" value="cancel_musicgpt" class="danger">Cancel Generation</button>
{% endif %}
{% if req.musicgpt_task_id and req.musicgpt_status not in ('IN_QUEUE', 'IN_PROGRESS') %}
<button type="submit" formaction="{{ url_for('admin_musicgpt_poll_request', rid=req.id) }}" class="secondary">Poll MusicGPT</button>
{% endif %}
</div>
</form>
</div>
<!-- Upload Songs -->
<div class="section">
<h2>2. Songs</h2>
<form method="POST">
<input type="hidden" name="action" value="delete_songs">
<div class="song-row">
<input type="checkbox" id="delete_a" name="delete_song" value="a"
{% if not (req.song_a_path and file_exists(req.song_a_path)) %}disabled{% endif %}>
<label for="delete_a">Version A MP3:
{% if req.song_a_path and file_exists(req.song_a_path) %}
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_a_path) }}
{% if req.song_a_wav_path and file_exists(req.song_a_wav_path) %}
<br><span class="status-badge ok">WAV</span> {{ basename(req.song_a_wav_path) }}
{% endif %}
{% else %}
<span class="status-badge missing">❌ Not ready</span>
{% endif %}
</label>
</div>
<div class="song-row">
<input type="checkbox" id="delete_b" name="delete_song" value="b"
{% if not (req.song_b_path and file_exists(req.song_b_path)) %}disabled{% endif %}>
<label for="delete_b">Version B MP3:
{% if req.song_b_path and file_exists(req.song_b_path) %}
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_b_path) }}
{% if req.song_b_wav_path and file_exists(req.song_b_wav_path) %}
<br><span class="status-badge ok">WAV</span> {{ basename(req.song_b_wav_path) }}
{% endif %}
{% else %}
<span class="status-badge missing">❌ Not ready</span>
{% endif %}
</label>
</div>
<div class="actions">
<button type="submit" class="danger" onclick="return confirm('Delete selected song files? This will reset the request to Prompt Ready.')">Delete Selected Songs</button>
</div>
</form>
<hr style="border-color:#374151;margin:1rem 0">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload_songs">
<label for="song_a">Version A MP3 (override)</label>
<input type="file" id="song_a" name="song_a" accept="audio/mpeg,.mp3">
<label for="song_b">Version B MP3 (override)</label>
<input type="file" id="song_b" name="song_b" accept="audio/mpeg,.mp3">
<div class="actions">
<button type="submit">Upload Songs</button>
</div>
</form>
<hr style="border-color:#374151;margin:1rem 0">
<!-- Stems -->
<form method="POST">
<input type="hidden" name="action" value="generate_stems">
<label for="stems_source_url">Stems Source Audio URL</label>
<input type="url" id="stems_source_url" name="stems_source_url" placeholder="https://.../song.mp3" value="{{ req.song_a_path or '' }}">
<p class="copy-hint">Vocal + instrumental extraction: ~$0.018$0.084 USD per song on Pro plan.</p>
<label for="stems_status">Stems Status:</label>
<p>
{% if req.stems_status %}
<span class="status-badge {% if req.stems_status == 'COMPLETED' %}ok{% elif req.stems_status in ('FAILED','ERROR') %}missing{% endif %}">{{ req.stems_status }}</span>
{% if req.stems_cost is not none %}({{ format_musicgpt_cost(req.stems_cost) }}){% endif %}
{% else %}
<span class="status-badge missing">Not requested</span>
{% endif %}
</p>
{% if req.stems_error %}
<div class="flash error">{{ req.stems_error }}</div>
{% endif %}
{% if req.stems_url %}
<p><a href="{{ req.stems_url }}" target="_blank">Download stems →</a></p>
{% endif %}
{% set stems_in_progress = req.stems_status in ('IN_QUEUE', 'IN_PROGRESS') %}
<div class="actions">
<button type="submit" {% if stems_in_progress %}disabled{% endif %}>Generate Stems</button>
</div>
</form>
</div>
<!-- Notify Customer -->
<div class="section">
<h2>3. Notify Customer</h2>
<p>
Preview email:
{% if req.preview_sent_at %}
<span class="status-badge sent">✅ Sent</span> {{ req.preview_sent_at }}
{% else %}
<span class="status-badge missing">❌ Not sent yet</span>
{% endif %}
</p>
<p>Both songs must be uploaded first.</p>
<form method="POST">
<input type="hidden" name="action" value="notify_customer">
<button type="submit" {% if not (req.song_a_path and req.song_b_path) %}disabled{% endif %}>Send Preview Link</button>
</form>
{% if req.song_a_path and req.song_b_path %}
<p style="margin-top:.8rem">
Operator preview link:
<a href="{{ url_for('play', token=req.player_token) }}" target="_blank" rel="noopener noreferrer">Open customer player in new tab →</a>
</p>
{% endif %}
</div>
<!-- Payment &amp; Delivery -->
<div class="section">
<h2>4. Payment &amp; Delivery</h2>
<p>
Customer approved:
{% if req.customer_approved == 'none' %}
<span class="approved-box">Nothing yet</span>
{% else %}
<span class="approved-box">{{ (req.customer_approved or 'none').upper() }}</span>
{% endif %}
</p>
<p>
Delivery email:
{% if req.delivery_sent_at %}
<span class="status-badge sent">✅ Sent</span> {{ req.delivery_sent_at }}
{% else %}
<span class="status-badge missing">❌ Not sent yet</span>
{% endif %}
</p>
{% if req.square_payment_ref %}
<p><strong>Square Payment Reference:</strong> {{ req.square_payment_ref }}</p>
{% endif %}
<form method="POST">
<!-- Shared payment reference field -->
<label for="square_payment_ref">Square Payment Reference</label>
<input type="text" id="square_payment_ref" name="square_payment_ref" value="{{ req.square_payment_ref or '' }}" placeholder="e.g. sq0idp-... or receipt number">
<div class="actions">
<button type="submit" name="action" value="update_payment_ref" class="secondary">Update Payment Reference</button>
</div>
<hr style="border-color:#374151;margin:1rem 0">
<p style="color:#93c5fd;font-weight:600">Delivery options:</p>
<label for="deliver_wav" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
<input type="checkbox" id="deliver_wav" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %} style="width:auto;margin:0">
Include WAV files in delivery email
</label>
<hr style="border-color:#374151;margin:1rem 0">
<p style="color:#93c5fd;font-weight:600">Select files to deliver:</p>
{% set all_files = [] %}
{% if req.song_a_path and file_exists(req.song_a_path) %}
{% set _ = all_files.append(req.song_a_path) %}
{% endif %}
{% if req.song_b_path and file_exists(req.song_b_path) %}
{% set _ = all_files.append(req.song_b_path) %}
{% endif %}
{% if req.deliver_wav %}
{% if req.song_a_wav_path and file_exists(req.song_a_wav_path) %}
{% set _ = all_files.append(req.song_a_wav_path) %}
{% endif %}
{% if req.song_b_wav_path and file_exists(req.song_b_wav_path) %}
{% set _ = all_files.append(req.song_b_wav_path) %}
{% endif %}
{% endif %}
{% set files_to_show = all_files + extra_files %}
{% for fpath in files_to_show %}
<div class="file-select">
<input type="checkbox" id="file_{{ loop.index }}" name="deliver_file" value="{{ fpath }}"
{% if fpath in all_files %}checked{% endif %}
{% if req.customer_approved == 'none' %}disabled{% endif %}>
<label for="file_{{ loop.index }}"{% if req.customer_approved == 'none' %} style="opacity:.6"{% endif %}>{{ basename(fpath) }}</label>
{% if not (fpath == req.song_a_path or fpath == req.song_b_path) %}
<div class="old-rev">archived / revision file</div>
{% endif %}
</div>
{% else %}
<p>No files available. Upload songs first.</p>
{% endfor %}
<div class="actions">
<button type="submit" name="action" value="mark_paid_deliver" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid &amp; Deliver</button>
</div>
{% if req.customer_approved == 'none' %}
<p style="margin-top:.5rem;color:#f87171;font-weight:600">⚠️ Customer must approve a version before you can mark paid or deliver.</p>
{% endif %}
</form>
</div>
</div>
</div>
<script>
function copyPromptForHermes() {
const callbackUrl = {{ callback_url | tojson }};
const styleGenre = {{ req.style_genre | tojson }};
const styleParts = styleGenre ? styleGenre.split(', ') : [];
const decade = styleParts[0] || '';
const basic = styleParts[1] || '';
const additional = styleParts.slice(2).join(', ') || '';
const styleSentence = [decade && `${decade}-era`, basic, additional && `with ${additional} influences`].filter(Boolean).join(' ');
const data = {
request_id: {{ req.id | tojson }},
email: {{ req.email | tojson }},
name: {{ req.name | tojson }},
pronouns: {{ req.pronouns | tojson }},
hobbies: {{ req.hobbies | tojson }},
notable_facts: {{ req.notable_facts | tojson }},
style_genre: styleGenre,
vocal_gender: {{ req.vocal_gender | tojson }},
extra_requests: {{ req.extra_requests | tojson }},
stems_interest: {{ (req.stems_interest or 0) | tojson }}
};
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nGenerate an AI music prompt for this customer and POST it back to the Callback URL as JSON.\\n\\nCustomer data:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nExpected JSON response format:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
navigator.clipboard.writeText(text).then(() => alert("Copied! Paste into Hermes. Hermes will POST the generated prompt back to the Callback URL."));
}
function copyToClipboard(elementId, label) {
const el = document.getElementById(elementId);
el.select();
el.setSelectionRange(0, 99999);
navigator.clipboard.writeText(el.value).then(() => alert(label + " copied!"));
}
function copyRevisionPromptForHermes() {
const callbackUrl = {{ callback_url | tojson }};
const currentTitle = {{ req.suno_title | tojson }};
const currentStyle = {{ req.suno_style | tojson }};
const currentLyrics = {{ req.suno_lyrics | tojson }};
const revisionNote = {{ req.revision_note | tojson }};
const revisionCount = {{ req.revision_count | tojson }};
const styleGenre = {{ req.style_genre | tojson }};
const styleParts = styleGenre ? styleGenre.split(', ') : [];
const decade = styleParts[0] || '';
const basic = styleParts[1] || '';
const additional = styleParts.slice(2).join(', ') || '';
const styleSentence = [decade && `${decade}-era`, basic, additional && `with ${additional} influences`].filter(Boolean).join(' ');
const data = {
request_id: {{ req.id | tojson }},
email: {{ req.email | tojson }},
name: {{ req.name | tojson }},
pronouns: {{ req.pronouns | tojson }},
hobbies: {{ req.hobbies | tojson }},
notable_facts: {{ req.notable_facts | tojson }},
style_genre: styleGenre,
vocal_gender: {{ req.vocal_gender | tojson }},
extra_requests: {{ req.extra_requests | tojson }},
stems_interest: {{ (req.stems_interest or 0) | tojson }}
};
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nThis is REVISION #${revisionCount || 1} for this customer.\\n\\nGenerate a NEW AI music prompt that addresses the following revision request, while keeping the same overall theme/persona and matching the customer's original brief as closely as possible.\\n\\nOriginal brief:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nPreviously generated prompt (do not copy verbatim; adapt and improve):\\nTitle: ${currentTitle || '-'}\\nStyle: ${currentStyle || '-'}\\nLyrics:\\n${currentLyrics || '-'}\\n\\nRevision request from customer:\\n${revisionNote || '-'}\\n\\nPOST the new prompt back to the Callback URL as JSON with this shape:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
navigator.clipboard.writeText(text).then(() => alert("Revision prompt copied! Paste into Hermes. Hermes will POST the revised prompt back to the Callback URL."));
}
</script>
</div>
</body>
</html>