Define techno-ethnic taste lane and notify when generation is ready.
Bonobo, Jamaica dub, Sahara, Mongolia overtone, and Urdu colour in settings and DJ prompts. Generate runs in background with polling, ready toast, optional browser notification, and autoplay of the new track. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+94
-10
@@ -299,7 +299,30 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
.status.generating { color: var(--accent); }
|
||||
.status.ready-flash { color: #6bcf8e; font-weight: 600; }
|
||||
.status.error { color: #ff5c7a; }
|
||||
.gen-ready-toast {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
background: linear-gradient(135deg, #1e3a2f, #2a4a3a);
|
||||
border: 1px solid #6bcf8e;
|
||||
color: #e8fff0;
|
||||
padding: 0.85rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.45);
|
||||
animation: toast-in 0.35s ease;
|
||||
max-width: 90vw;
|
||||
text-align: center;
|
||||
}
|
||||
@keyframes toast-in {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(-12px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
.stack {
|
||||
font-size: 0.7rem;
|
||||
color: #555568;
|
||||
@@ -626,6 +649,7 @@
|
||||
</div>
|
||||
|
||||
<div class="status" id="status">Connecting…</div>
|
||||
<div id="genReadyToast" class="gen-ready-toast" hidden></div>
|
||||
|
||||
<button type="button" class="booth-toggle" id="boothToggle">🎙 Vocal booth — record your layer (Chrome)</button>
|
||||
|
||||
@@ -674,7 +698,10 @@
|
||||
const moodEl = document.getElementById('mood');
|
||||
const djEl = document.getElementById('dj');
|
||||
const statusEl = document.getElementById('status');
|
||||
const genReadyToast = document.getElementById('genReadyToast');
|
||||
const genBtn = document.getElementById('genBtn');
|
||||
let pollStartedAt = null;
|
||||
let lastNotifiedCompletedAt = null;
|
||||
const skipBtn = document.getElementById('skipBtn');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const chatLog = document.getElementById('chatLog');
|
||||
@@ -978,8 +1005,11 @@
|
||||
const costs = stats.costs || {};
|
||||
statSpent.textContent = fmtUsd(today.estimated_usd);
|
||||
statPerTrack.textContent = `~${fmtUsd(costs.per_track_estimate_usd)} / track`;
|
||||
statGenerated.textContent = `${today.generated || 0} / ${today.max_per_day || 10}`;
|
||||
statRemaining.textContent = `${today.remaining ?? 0} remaining`;
|
||||
const unlimited = today.unlimited || today.max_per_day === 0;
|
||||
statGenerated.textContent = unlimited
|
||||
? `${today.generated || 0} (unlimited)`
|
||||
: `${today.generated || 0} / ${today.max_per_day || 10}`;
|
||||
statRemaining.textContent = unlimited ? '∞' : `${today.remaining ?? 0} remaining`;
|
||||
statMaxBudget.textContent = fmtUsd(stats.projected_daily_max_usd);
|
||||
if (stats.playback) {
|
||||
radioSettings.playback = stats.playback;
|
||||
@@ -1161,22 +1191,63 @@
|
||||
}
|
||||
|
||||
let pollTimer = null;
|
||||
let toastTimer = null;
|
||||
|
||||
function showReadyToast(title) {
|
||||
genReadyToast.textContent = `Ready · ${title}`;
|
||||
genReadyToast.hidden = false;
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { genReadyToast.hidden = true; }, 8000);
|
||||
}
|
||||
|
||||
function notifyTrackReady(track, gen) {
|
||||
const title = track?.title || gen?.last_completed_title || 'New track';
|
||||
statusEl.textContent = `Ready · playing ${title}`;
|
||||
statusEl.classList.remove('generating', 'error');
|
||||
statusEl.classList.add('ready-flash');
|
||||
setTimeout(() => statusEl.classList.remove('ready-flash'), 5000);
|
||||
showReadyToast(title);
|
||||
addBubble('dj', `Fresh cut ready: ${title} — on air now.`);
|
||||
if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
|
||||
new Notification('Live Ozan Radio', { body: `Ready: ${title}`, tag: 'track-ready' });
|
||||
}
|
||||
lastNotifiedCompletedAt = gen?.last_completed_at || null;
|
||||
}
|
||||
|
||||
function pollForNewTrack() {
|
||||
let n = 0;
|
||||
pollStartedAt = Date.now();
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = setInterval(async () => {
|
||||
n += 1;
|
||||
const stats = await loadStats(true);
|
||||
if (stats?.generation?.busy) return;
|
||||
if (stats?.generation?.error) {
|
||||
const gen = stats?.generation;
|
||||
if (gen?.busy) return;
|
||||
if (gen?.error) {
|
||||
clearInterval(pollTimer);
|
||||
statusEl.textContent = gen.error;
|
||||
statusEl.classList.add('error');
|
||||
addBubble('dj', `Couldn't finish that track: ${gen.error}`);
|
||||
return;
|
||||
}
|
||||
const completedAt = gen?.last_completed_at;
|
||||
const isNewCompletion = completedAt
|
||||
&& completedAt !== lastNotifiedCompletedAt
|
||||
&& (!pollStartedAt || new Date(completedAt).getTime() >= pollStartedAt - 2000);
|
||||
await refreshNow();
|
||||
const now = await fetch(`${API}/api/now`).then(r => r.json()).catch(() => null);
|
||||
if (isNewCompletion && now?.track) {
|
||||
applyTrack(now.track, 'generated');
|
||||
notifyTrackReady(now.track, gen);
|
||||
clearInterval(pollTimer);
|
||||
return;
|
||||
}
|
||||
await refreshNow();
|
||||
const now = await fetch(`${API}/api/now`).then(r => r.json()).catch(() => null);
|
||||
if (now?.track?.track_id !== currentTrackId) clearInterval(pollTimer);
|
||||
if (n > 60) clearInterval(pollTimer);
|
||||
}, 3000);
|
||||
if (n > 90) {
|
||||
clearInterval(pollTimer);
|
||||
statusEl.textContent = 'Generation taking longer than usual — still polling…';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async function refreshNow() {
|
||||
@@ -1197,22 +1268,35 @@
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
genBtn.disabled = true;
|
||||
statusEl.textContent = 'DeepSeek is planning… Lyria is composing…';
|
||||
statusEl.textContent = 'Starting… DeepSeek plans, then Lyria composes (~30–90s)';
|
||||
statusEl.classList.add('generating');
|
||||
try {
|
||||
const res = await fetch(`${API}/api/generate`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.status === 'busy') {
|
||||
statusEl.textContent = data.message;
|
||||
applyGenerationState(data.generation);
|
||||
pollForNewTrack();
|
||||
} else if (data.status === 'limit') {
|
||||
statusEl.textContent = data.message || 'Daily limit reached';
|
||||
statusEl.classList.remove('generating');
|
||||
updateDashboard({ today: data.budget, costs: radioSettings.costs });
|
||||
} else if (data.status === 'error') {
|
||||
statusEl.textContent = data.message || 'Generation failed';
|
||||
statusEl.classList.add('error');
|
||||
applyGenerationState(data.generation || { error: data.message });
|
||||
} else if (data.status === 'accepted' || data.generating) {
|
||||
applyGenerationState(data.generation || { busy: true, phase: 'planning' });
|
||||
pollForNewTrack();
|
||||
} else if (data.status === 'ok') {
|
||||
if (data.track) applyTrack(data.track, data.source || 'generated');
|
||||
if (data.track) {
|
||||
applyTrack(data.track, data.source || 'generated');
|
||||
notifyTrackReady(data.track, data.generation);
|
||||
}
|
||||
await loadStats();
|
||||
} else {
|
||||
statusEl.textContent = 'Generation failed — check server logs';
|
||||
|
||||
|
Before
After
|
Reference in New Issue
Block a user