573 lines
32 KiB
HTML
573 lines
32 KiB
HTML
<!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>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/booth.css') }}">
|
||
</head>
|
||
<body class="booth-page">
|
||
<div class="container container-wide">
|
||
<p class="mb-2"><a href="{{ url_for('admin_dashboard') }}">← Dashboard</a></p>
|
||
<h1>Request #{{ req.id }} — {{ req.name }}</h1>
|
||
|
||
<div class="section-nav">
|
||
<a href="#customer">Customer</a>
|
||
<a href="#prompt">Prompt</a>
|
||
<a href="#songs">Songs</a>
|
||
<a href="#notify">Notify</a>
|
||
<a href="#delivery">Payment & Delivery</a>
|
||
<a href="#notes">Notes</a>
|
||
</div>
|
||
|
||
<!-- Status panel -->
|
||
<div class="section" id="status">
|
||
<div class="media-status">
|
||
{% if req.album_cover_url %}
|
||
<div class="cover-col">
|
||
<img src="{{ req.album_cover_url }}" alt="Album cover">
|
||
{% if not deliver_album_cover %}
|
||
<p class="cover-hint">Album cover is hidden from the customer preview/delivery because delivery is disabled in settings.</p>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
<div class="status-col">
|
||
<p class="text-muted" style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;margin:0">Request Status</p>
|
||
{% if current_step == -1 %}
|
||
<p class="mt-2"><span class="status-badge cancelled">Cancelled</span></p>
|
||
{% else %}
|
||
<ol class="steps" aria-label="Request progress">
|
||
{% for i in range(steps|length) %}
|
||
{% set key, label = steps[i] %}
|
||
{% set is_last = i == steps|length - 1 %}
|
||
{% set is_completed = i < current_step or (is_last and current_step == i) %}
|
||
{% set is_current = i == current_step and not is_last %}
|
||
<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 class="text-muted" style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;margin-top:var(--space-4)">Recording Studio Status</p>
|
||
<ol class="steps" aria-label="MusicGPT progress">
|
||
{% for j in range(mg_steps|length) %}
|
||
{% set mg_key, mg_label = mg_steps[j] %}
|
||
<li class="step-item {% if mg_index > j or mg_completed %}completed{% elif mg_index == j %}current{% else %}upcoming{% endif %}" {% if mg_index == j %}aria-current="step"{% endif %}>
|
||
<div class="step-indicator">{% if mg_index > j or mg_completed %}✓{% else %}{{ j + 1 }}{% endif %}</div>
|
||
<div class="step-label">{{ mg_label }}</div>
|
||
</li>
|
||
{% endfor %}
|
||
</ol>
|
||
|
||
{% set total_cost = (req.musicgpt_cost or 0) + (req.stems_cost or 0) %}
|
||
{% if total_cost > 0 %}
|
||
<p class="mt-2"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(total_cost) }}
|
||
{% if req.stems_cost %}
|
||
<span class="text-muted" style="font-size:0.75rem"> (songs: {{ format_musicgpt_cost(req.musicgpt_cost) }} + stems: {{ format_musicgpt_cost(req.stems_cost) }})</span>
|
||
{% endif %}
|
||
</p>
|
||
{% elif req.musicgpt_cost is not none %}
|
||
<p class="mt-2"><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 }}?')" class="mt-4">
|
||
<input type="hidden" name="action" value="cancel_request">
|
||
<button type="submit" class="button-danger">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">
|
||
<div class="section" id="customer">
|
||
<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 class="required">*</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 & 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 class="required">*</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 class="required">*</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 class="copy-hint">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" class="checkbox-label">
|
||
<input type="checkbox" id="customer_stems_interest" name="stems_interest" value="1" {% if req.stems_interest %}checked{% endif %}>
|
||
Customer is interested in STEMS / multitrack files
|
||
</label>
|
||
|
||
<div class="actions">
|
||
<button type="submit" class="button-secondary">Save Customer Info</button>
|
||
</div>
|
||
</form>
|
||
|
||
<p class="mt-4 mb-0"><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()" class="button-secondary">Copy revision prompt for Hermes</button>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="section" id="history">
|
||
<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>
|
||
|
||
<div class="section" id="notes">
|
||
<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="button-secondary">Save Notes</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="section" id="stems">
|
||
<h2>Stems / Extras Link</h2>
|
||
<p class="copy-hint">Auto-generated from Gokapi when stems are completed. You can also paste a custom share link here — it will appear in the delivery email.</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/d?id=...">
|
||
<div class="actions">
|
||
<button type="submit" class="button-secondary">Save Link</button>
|
||
{% if req.stems_url and not req.stems_link %}
|
||
<button type="submit" name="action" value="reprocess_stems" class="button-secondary">Re-upload to Gokapi</button>
|
||
{% endif %}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT COLUMN -->
|
||
<div class="right">
|
||
<div class="section" id="prompt">
|
||
<h2>1. Generate & Save Music Prompt</h2>
|
||
<button type="button" onclick="copyPromptForHermes()" class="button-secondary">Copy customer info for Hermes</button>
|
||
<form method="POST" action="{{ url_for('send_to_hermes', rid=req.id) }}" style="display:inline">
|
||
<button type="submit" onclick="return confirm('Send this request to Hermes to generate a MusicGPT prompt?')">Send to Hermes</button>
|
||
</form>
|
||
<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="button-secondary" onclick="copyToClipboard('suno_lyrics', 'Lyrics')">Copy Lyrics</button>
|
||
<button type="button" class="button-secondary" onclick="copyToClipboard('suno_style', 'Style')">Copy Style</button>
|
||
<button type="button" class="button-secondary" onclick="copyToClipboard('suno_title', 'Title')">Copy Title</button>
|
||
<button type="submit">Save Prompt</button>
|
||
</div>
|
||
</form>
|
||
|
||
<hr class="divider">
|
||
|
||
<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" class="checkbox-label">
|
||
<input type="checkbox" id="deliver_wav_gen" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %}>
|
||
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="button-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="button-secondary">Poll MusicGPT</button>
|
||
{% endif %}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="section" id="songs">
|
||
<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="button-danger" onclick="return confirm('Delete selected song files? This will reset the request to Prompt Ready.')">Delete Selected Songs</button>
|
||
</div>
|
||
</form>
|
||
|
||
<hr class="divider">
|
||
|
||
<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 class="divider">
|
||
|
||
<form method="POST">
|
||
<input type="hidden" name="action" value="generate_stems">
|
||
<p style="margin:0 0 var(--space-2) 0;font-weight:600">Select source MP3 for stems extraction:</p>
|
||
{% set a_ready = req.song_a_path and file_exists(req.song_a_path) %}
|
||
{% set b_ready = req.song_b_path and file_exists(req.song_b_path) %}
|
||
<div class="song-row">
|
||
<input type="radio" id="stems_a" name="stems_source" value="a" {% if not a_ready %}disabled{% endif %} {% if a_ready and not b_ready %}checked{% endif %}>
|
||
<label for="stems_a" {% if not a_ready %}style="opacity:.6"{% endif %}>Version A MP3:
|
||
{% if a_ready %}
|
||
<span class="status-badge ok">Ready</span> {{ basename(req.song_a_path) }}
|
||
{% else %}
|
||
<span class="status-badge missing">Not ready</span>
|
||
{% endif %}
|
||
</label>
|
||
</div>
|
||
<div class="song-row">
|
||
<input type="radio" id="stems_b" name="stems_source" value="b" {% if not b_ready %}disabled{% endif %} {% if b_ready %}checked{% endif %}>
|
||
<label for="stems_b" {% if not b_ready %}style="opacity:.6"{% endif %}>Version B MP3:
|
||
{% if b_ready %}
|
||
<span class="status-badge ok">Ready</span> {{ basename(req.song_b_path) }}
|
||
{% else %}
|
||
<span class="status-badge missing">Not ready</span>
|
||
{% endif %}
|
||
</label>
|
||
</div>
|
||
<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 class="mt-0">
|
||
{% 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_link %}
|
||
<div class="actions">
|
||
<a href="{{ req.stems_link }}" target="_blank" rel="noopener noreferrer" class="button-link">⬇ Download Stems</a>
|
||
</div>
|
||
{% elif req.stems_url %}
|
||
<div class="actions">
|
||
<a href="{{ url_for('admin_download_stems', rid=req.id) }}" class="button-link">⬇ Download Stems (zip)</a>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% set stems_in_progress = req.stems_status in ('IN_QUEUE', 'IN_PROGRESS') %}
|
||
<div class="actions">
|
||
<button type="submit" {% if stems_in_progress or not (a_ready or b_ready) %}disabled{% endif %}>Generate Stems</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="section" id="notify">
|
||
<h2>3. Notify Customer</h2>
|
||
<p class="mt-0">
|
||
Preview email:
|
||
{% if req.preview_sent_at %}
|
||
<span class="status-badge sent">Sent</span> {{ req.preview_sent_at|regina_dt }}
|
||
{% else %}
|
||
<span class="status-badge missing">Not sent yet</span>
|
||
{% endif %}
|
||
</p>
|
||
<p class="copy-hint">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 class="mt-4 mb-0">
|
||
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>
|
||
|
||
<div class="section" id="delivery">
|
||
<h2>4. Payment & Delivery</h2>
|
||
<p class="mt-0">
|
||
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|regina_dt }}
|
||
{% 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">
|
||
<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="button-secondary">Update Payment Reference</button>
|
||
</div>
|
||
|
||
<hr class="divider">
|
||
|
||
<p style="color:var(--text-link);font-weight:600">Delivery options:</p>
|
||
<label for="deliver_wav" class="checkbox-label">
|
||
<input type="checkbox" id="deliver_wav" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %}>
|
||
Include WAV files in delivery email
|
||
</label>
|
||
|
||
<hr class="divider">
|
||
|
||
<p style="color:var(--text-link);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="button-success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
||
</div>
|
||
{% if req.customer_approved == 'none' %}
|
||
<p class="text-danger" style="margin-top:var(--space-2);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>
|