Refactor activity system: energy-based probability, manual override, all 5 activity types
- Rewrite utils/activities.py with mood energy-driven activity probability (high-energy moods like excited/bubbly show activity ~80-85% of the time, low-energy moods like sleepy/melancholy only ~15-25%) - Add manual override system with 30-min auto-expiry for Web UI control - Support all 5 Discord activity types: listening, playing, watching, competing, streaming (with purple LIVE badge via discord.Streaming) - Add current activity tracking (get_current_activity) - Add force=True param to update_bot_presence for on_ready (bot.py) - Add 4 new API routes for manual override: GET/POST/DELETE /activities/current, POST /activities/current/auto - Expand activities.yaml from 139 to 157 entries, adding watching, competing, and streaming entries across 11 moods - Update Web UI: activity type dropdown with all 5 types, conditional URL field for streaming, 'Current Activity' override panel with set/clear/auto controls, type-aware icons and labels
This commit is contained in:
@@ -481,7 +481,7 @@
|
||||
.act-entry-icon { font-size: 1.1rem; min-width: 24px; text-align: center; }
|
||||
.act-entry input[type="text"] { flex: 1; }
|
||||
.act-entry input[type="number"] { width: 55px; }
|
||||
.act-entry select { width: 110px; }
|
||||
.act-entry select { width: 130px; }
|
||||
.act-toolbar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@@ -1362,6 +1362,35 @@
|
||||
<span id="activities-status" style="font-size: 0.85rem; color: #61dafb;"></span>
|
||||
</div>
|
||||
|
||||
<!-- Current Activity Override Panel -->
|
||||
<div style="margin-bottom: 1rem; padding: 0.75rem; background: #252535; border-radius: 6px; border: 1px solid #3a3a5a;">
|
||||
<h4 style="margin: 0 0 0.5rem 0; color: #61dafb; font-size: 0.95rem;">🎯 Current Activity</h4>
|
||||
<div id="activity-override-status" style="margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa;">
|
||||
Loading...
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem;">
|
||||
<button onclick="activityRefreshCurrent()" style="background: #555; font-size: 0.8rem; padding: 0.3rem 0.6rem;">🔄 Refresh</button>
|
||||
<button onclick="activityReleaseAuto()" style="background: #27ae60; font-size: 0.8rem; padding: 0.3rem 0.6rem;">🌀 Return to Auto</button>
|
||||
</div>
|
||||
<details style="margin-top: 0.5rem;">
|
||||
<summary style="cursor: pointer; font-size: 0.85rem; color: #61dafb;">✏️ Manual Override</summary>
|
||||
<div style="margin-top: 0.5rem; display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center;">
|
||||
<select id="act-manual-type" style="width: 120px; padding: 0.3rem;">
|
||||
<option value="listening">🎵 Listening</option>
|
||||
<option value="playing">🎮 Playing</option>
|
||||
<option value="watching">📺 Watching</option>
|
||||
<option value="competing">🏆 Competing</option>
|
||||
<option value="streaming">🔴 Streaming</option>
|
||||
</select>
|
||||
<input type="text" id="act-manual-name" placeholder="Activity name" style="flex: 2; min-width: 120px; padding: 0.3rem;">
|
||||
<input type="text" id="act-manual-state" placeholder="Detail (optional)" style="flex: 1; min-width: 80px; padding: 0.3rem;">
|
||||
<input type="text" id="act-manual-url" placeholder="URL (streaming)" style="flex: 1; min-width: 80px; padding: 0.3rem; display: none;">
|
||||
<button onclick="activitySetManual()" style="background: #e67e22; font-size: 0.8rem; padding: 0.3rem 0.8rem;">Set</button>
|
||||
<button onclick="activityClearManual()" style="background: #c0392b; font-size: 0.8rem; padding: 0.3rem 0.8rem;">Clear</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Normal Moods subsection -->
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<div style="cursor: pointer; user-select: none; padding: 0.5rem; background: #2a2a2a; border-radius: 4px; margin-bottom: 0.5rem;" onclick="activitiesSectionToggle('normal')">
|
||||
@@ -6819,7 +6848,10 @@ function activitiesToggle() {
|
||||
activitiesOpen = !activitiesOpen;
|
||||
document.getElementById('activities-body').style.display = activitiesOpen ? 'block' : 'none';
|
||||
document.getElementById('activities-toggle-icon').textContent = activitiesOpen ? '▼' : '▶';
|
||||
if (activitiesOpen && !activitiesData) activitiesLoad();
|
||||
if (activitiesOpen) {
|
||||
if (!activitiesData) activitiesLoad();
|
||||
activityRefreshCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
function activitiesSectionToggle(section) {
|
||||
@@ -6857,11 +6889,19 @@ function activitiesRenderSection(section) {
|
||||
const isEditing = activitiesEditing[key];
|
||||
const songs = entries.filter(e => e.type === 'listening').length;
|
||||
const games = entries.filter(e => e.type === 'playing').length;
|
||||
const watches = entries.filter(e => e.type === 'watching').length;
|
||||
const competes = entries.filter(e => e.type === 'competing').length;
|
||||
const streams = entries.filter(e => e.type === 'streaming').length;
|
||||
|
||||
let stats = `${songs}🎵 ${games}🎮`;
|
||||
if (watches) stats += ` ${watches}📺`;
|
||||
if (competes) stats += ` ${competes}🏆`;
|
||||
if (streams) stats += ` ${streams}🔴`;
|
||||
|
||||
html += `<div class="act-mood-row">`;
|
||||
html += `<div class="act-mood-header" onclick="activitiesMoodToggle('${section}','${mood}')">`;
|
||||
html += `<span class="act-mood-name"><span id="act-icon-${section}-${mood}">▶</span> ${mood}</span>`;
|
||||
html += `<span class="act-mood-stats">${songs}🎵 ${games}🎮</span>`;
|
||||
html += `<span class="act-mood-stats">${stats}</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="act-mood-content" id="act-content-${section}-${mood}">`;
|
||||
|
||||
@@ -6879,11 +6919,13 @@ function activitiesRenderSection(section) {
|
||||
function activitiesRenderView(section, mood, entries) {
|
||||
let html = '';
|
||||
for (const entry of entries) {
|
||||
const icon = entry.type === 'listening' ? '🎵' : '🎮';
|
||||
const label = entry.type === 'listening' ? 'Listening to' : 'Playing';
|
||||
const icons = { listening: '🎵', playing: '🎮', watching: '📺', competing: '🏆', streaming: '🔴' };
|
||||
const labels = { listening: 'Listening to', playing: 'Playing', watching: 'Watching', competing: 'Competing in', streaming: 'Streaming' };
|
||||
const icon = icons[entry.type] || '🎮';
|
||||
const label = labels[entry.type] || 'Playing';
|
||||
html += `<div class="act-entry">`;
|
||||
html += `<span class="act-entry-icon">${icon}</span>`;
|
||||
html += `<span style="flex:1;">${escapeHtml(entry.name)}`;
|
||||
html += `<span style="flex:1;"><strong style="color:#61dafb; font-size:0.8rem;">${label}</strong> ${escapeHtml(entry.name)}`;
|
||||
if (entry.state) html += ` <span style="color:#aaa; font-size:0.85rem;">— ${escapeHtml(entry.state)}</span>`;
|
||||
html += `</span>`;
|
||||
html += `<span style="color:#888; font-size:0.8rem;">weight: ${entry.weight}</span>`;
|
||||
@@ -6900,12 +6942,16 @@ function activitiesRenderEditForm(section, mood, entries) {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const e = entries[i];
|
||||
html += `<div class="act-entry">`;
|
||||
html += `<select id="act-type-${section}-${mood}-${i}" value="${e.type}">`;
|
||||
html += `<select id="act-type-${section}-${mood}-${i}" onchange="activitiesTypeChanged('${section}','${mood}',${i})">`;
|
||||
html += `<option value="listening" ${e.type === 'listening' ? 'selected' : ''}>🎵 Listening</option>`;
|
||||
html += `<option value="playing" ${e.type === 'playing' ? 'selected' : ''}>🎮 Playing</option>`;
|
||||
html += `<option value="watching" ${e.type === 'watching' ? 'selected' : ''}>📺 Watching</option>`;
|
||||
html += `<option value="competing" ${e.type === 'competing' ? 'selected' : ''}>🏆 Competing</option>`;
|
||||
html += `<option value="streaming" ${e.type === 'streaming' ? 'selected' : ''}>🔴 Streaming</option>`;
|
||||
html += `</select>`;
|
||||
html += `<input type="text" id="act-name-${section}-${mood}-${i}" value="${escapeHtml(e.name)}" placeholder="Song/Game name" style="flex:2; min-width:120px;">`;
|
||||
html += `<input type="text" id="act-state-${section}-${mood}-${i}" value="${escapeHtml(e.state || '')}" placeholder="Artist / Genre (optional)" style="flex:1.5; min-width:100px;">`;
|
||||
html += `<input type="text" id="act-name-${section}-${mood}-${i}" value="${escapeHtml(e.name)}" placeholder="Name" style="flex:2; min-width:120px;">`;
|
||||
html += `<input type="text" id="act-state-${section}-${mood}-${i}" value="${escapeHtml(e.state || '')}" placeholder="Detail (optional)" style="flex:1.5; min-width:100px;">`;
|
||||
html += `<input type="text" id="act-url-${section}-${mood}-${i}" value="${escapeHtml(e.url || '')}" placeholder="URL (streaming)" style="flex:1.5; min-width:100px; ${e.type === 'streaming' ? '' : 'display:none;'}">`;
|
||||
html += `<input type="number" id="act-weight-${section}-${mood}-${i}" value="${e.weight}" min="1" max="20" style="width:60px;">`;
|
||||
html += `<button onclick="activitiesRemoveEntry('${section}','${mood}',${i})" style="background:#c0392b; padding:0.3rem 0.5rem;" title="Remove">✕</button>`;
|
||||
html += `</div>`;
|
||||
@@ -6918,6 +6964,13 @@ function activitiesRenderEditForm(section, mood, entries) {
|
||||
return html;
|
||||
}
|
||||
|
||||
function activitiesTypeChanged(section, mood, index) {
|
||||
const typeEl = document.getElementById(`act-type-${section}-${mood}-${index}`);
|
||||
const urlEl = document.getElementById(`act-url-${section}-${mood}-${index}`);
|
||||
if (!typeEl || !urlEl) return;
|
||||
urlEl.style.display = typeEl.value === 'streaming' ? '' : 'none';
|
||||
}
|
||||
|
||||
function activitiesMoodToggle(section, mood) {
|
||||
const el = document.getElementById(`act-content-${section}-${mood}`);
|
||||
const iconEl = document.getElementById(`act-icon-${section}-${mood}`);
|
||||
@@ -6975,10 +7028,12 @@ function activitiesSyncFormToCache(section, mood) {
|
||||
const typeEl = document.getElementById(`act-type-${section}-${mood}-${i}`);
|
||||
const nameEl = document.getElementById(`act-name-${section}-${mood}-${i}`);
|
||||
const stateEl = document.getElementById(`act-state-${section}-${mood}-${i}`);
|
||||
const urlEl = document.getElementById(`act-url-${section}-${mood}-${i}`);
|
||||
const weightEl = document.getElementById(`act-weight-${section}-${mood}-${i}`);
|
||||
if (typeEl) entries[i].type = typeEl.value;
|
||||
if (nameEl) entries[i].name = nameEl.value;
|
||||
if (stateEl) entries[i].state = stateEl.value || undefined;
|
||||
if (urlEl) entries[i].url = urlEl.value || undefined;
|
||||
if (weightEl) entries[i].weight = parseInt(weightEl.value) || 1;
|
||||
}
|
||||
activitiesEditCache[key] = entries;
|
||||
@@ -6999,6 +7054,10 @@ async function activitiesSave(section, mood) {
|
||||
showNotification(`Entry ${i + 1}: weight must be at least 1`, 'error');
|
||||
return;
|
||||
}
|
||||
if (entries[i].type === 'streaming' && !entries[i].url) {
|
||||
showNotification(`Entry ${i + 1}: streaming requires a URL`, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -7013,6 +7072,88 @@ async function activitiesSave(section, mood) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CURRENT ACTIVITY OVERRIDE
|
||||
// ============================================================================
|
||||
|
||||
function _activityTypeIcon(type) {
|
||||
return { listening: '🎵', playing: '🎮', watching: '📺', competing: '🏆', streaming: '🔴' }[type] || '🎮';
|
||||
}
|
||||
|
||||
function _activityTypeLabel(type) {
|
||||
return { listening: 'Listening to', playing: 'Playing', watching: 'Watching', competing: 'Competing in', streaming: 'Streaming' }[type] || 'Playing';
|
||||
}
|
||||
|
||||
async function activityRefreshCurrent() {
|
||||
const statusEl = document.getElementById('activity-override-status');
|
||||
try {
|
||||
const data = await apiCall('/activities/current');
|
||||
const act = data.activity;
|
||||
const isOverride = data.manual_override;
|
||||
|
||||
if (act) {
|
||||
const icon = _activityTypeIcon(act.type);
|
||||
const label = _activityTypeLabel(act.type);
|
||||
let html = `${icon} <strong>${label}</strong> ${escapeHtml(act.name)}`;
|
||||
if (act.state) html += ` <span style="color:#aaa;">— ${escapeHtml(act.state)}</span>`;
|
||||
if (isOverride) html += ` <span style="color:#e67e22; font-size:0.8rem;">⚡ MANUAL OVERRIDE (30 min)</span>`;
|
||||
statusEl.innerHTML = html;
|
||||
} else {
|
||||
let html = '<span style="color:#888;">No activity (idle)</span>';
|
||||
if (isOverride) html += ' <span style="color:#e67e22; font-size:0.8rem;">⚡ MANUAL OVERRIDE</span>';
|
||||
statusEl.innerHTML = html;
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.innerHTML = `<span style="color:#e74c3c;">Error: ${e.message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function activitySetManual() {
|
||||
const type = document.getElementById('act-manual-type').value;
|
||||
const name = document.getElementById('act-manual-name').value.trim();
|
||||
const state = document.getElementById('act-manual-state').value.trim();
|
||||
const url = document.getElementById('act-manual-url').value.trim();
|
||||
|
||||
if (!name) { showNotification('Activity name is required', 'error'); return; }
|
||||
if (type === 'streaming' && !url) { showNotification('Streaming requires a URL', 'error'); return; }
|
||||
|
||||
try {
|
||||
const body = { type, name };
|
||||
if (state) body.state = state;
|
||||
if (url) body.url = url;
|
||||
await apiCall('/activities/current', 'POST', body);
|
||||
showNotification(`Set activity: ${type} ${name}`, 'success');
|
||||
await activityRefreshCurrent();
|
||||
} catch (e) {
|
||||
showNotification('Failed to set activity: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function activityClearManual() {
|
||||
try {
|
||||
await apiCall('/activities/current', 'DELETE');
|
||||
showNotification('Activity cleared (manual override active)', 'success');
|
||||
await activityRefreshCurrent();
|
||||
} catch (e) {
|
||||
showNotification('Failed to clear: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function activityReleaseAuto() {
|
||||
try {
|
||||
await apiCall('/activities/current/auto', 'POST');
|
||||
showNotification('Returned to automatic mode', 'success');
|
||||
await activityRefreshCurrent();
|
||||
} catch (e) {
|
||||
showNotification('Failed to release override: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide URL field when streaming is selected in manual override
|
||||
document.getElementById('act-manual-type').addEventListener('change', function() {
|
||||
document.getElementById('act-manual-url').style.display = this.value === 'streaming' ? '' : 'none';
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user