diff --git a/bot/static/index.html b/bot/static/index.html
index 3c19af0..aa6b000 100644
--- a/bot/static/index.html
+++ b/bot/static/index.html
@@ -450,6 +450,46 @@
color: #ddd;
}
+ /* Mood Activities Editor */
+ .act-mood-row {
+ margin-bottom: 0.5rem;
+ border: 1px solid #3a3a3a;
+ border-radius: 4px;
+ overflow: hidden;
+ }
+ .act-mood-header {
+ cursor: pointer;
+ user-select: none;
+ padding: 0.5rem 0.75rem;
+ background: #2a2a2a;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+ .act-mood-header:hover { background: #333; }
+ .act-mood-header .act-mood-name { font-weight: bold; min-width: 120px; }
+ .act-mood-header .act-mood-stats { color: #888; font-size: 0.8rem; }
+ .act-mood-content { display: none; padding: 0.75rem; background: #1e1e1e; }
+ .act-entry {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.35rem 0;
+ border-bottom: 1px solid #333;
+ }
+ .act-entry:last-child { border-bottom: none; }
+ .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-toolbar {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+ padding-top: 0.5rem;
+ border-top: 1px solid #444;
+ }
+
.tab-content {
display: none;
}
@@ -1328,6 +1368,40 @@
+
+
+
+
+
▶ 🎵 Mood Activities
+
+
+
+
+
+
+
+
+
+
+
+ ▶ 😇 Normal Moods
+
+
+
+
+
+
+
+
@@ -6731,6 +6805,209 @@ function escapeJsonForAttribute(obj) {
.replace(/>/g, '>');
}
+// ============================================================================
+// MOOD ACTIVITIES EDITOR
+// ============================================================================
+
+let activitiesData = null; // Full activities data from API
+let activitiesOpen = false; // Top-level accordion state
+let activitiesSections = { normal: false, evil: false }; // Section accordion state
+let activitiesEditing = {}; // Track which moods are in edit mode: { "normal/bubbly": true }
+let activitiesEditCache = {}; // Temp storage for edits: { "normal/bubbly": [...] }
+
+function activitiesToggle() {
+ activitiesOpen = !activitiesOpen;
+ document.getElementById('activities-body').style.display = activitiesOpen ? 'block' : 'none';
+ document.getElementById('activities-toggle-icon').textContent = activitiesOpen ? '▼' : '▶';
+ if (activitiesOpen && !activitiesData) activitiesLoad();
+}
+
+function activitiesSectionToggle(section) {
+ activitiesSections[section] = !activitiesSections[section];
+ document.getElementById(`activities-${section}-body`).style.display = activitiesSections[section] ? 'block' : 'none';
+ document.getElementById(`activities-${section}-icon`).textContent = activitiesSections[section] ? '▼' : '▶';
+}
+
+async function activitiesLoad() {
+ const statusEl = document.getElementById('activities-status');
+ statusEl.textContent = 'Loading...';
+ try {
+ activitiesData = await apiCall('/activities');
+ const normalMoods = Object.keys(activitiesData.normal || {});
+ const evilMoods = Object.keys(activitiesData.evil || {});
+ const total = normalMoods.length + evilMoods.length;
+ document.getElementById('activities-summary').textContent = `(${total} moods configured)`;
+ activitiesRenderSection('normal');
+ activitiesRenderSection('evil');
+ statusEl.textContent = '';
+ } catch (e) {
+ statusEl.textContent = 'Failed to load: ' + e.message;
+ statusEl.style.color = '#e74c3c';
+ }
+}
+
+function activitiesRenderSection(section) {
+ const container = document.getElementById(`activities-${section}-list`);
+ if (!activitiesData || !activitiesData[section]) { container.innerHTML = 'No data
'; return; }
+
+ const moods = activitiesData[section];
+ let html = '';
+ for (const [mood, entries] of Object.entries(moods)) {
+ const key = `${section}/${mood}`;
+ const isEditing = activitiesEditing[key];
+ const songs = entries.filter(e => e.type === 'listening').length;
+ const games = entries.filter(e => e.type === 'playing').length;
+
+ html += ``;
+ html += ``;
+ html += `
`;
+
+ if (isEditing) {
+ html += activitiesRenderEditForm(section, mood, activitiesEditCache[key] || entries);
+ } else {
+ html += activitiesRenderView(section, mood, entries);
+ }
+
+ html += `
`;
+ }
+ container.innerHTML = html;
+}
+
+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';
+ html += ``;
+ html += `${icon}`;
+ html += `${escapeHtml(entry.name)}`;
+ html += `weight: ${entry.weight}`;
+ html += `
`;
+ }
+ html += ``;
+ html += ``;
+ html += `
`;
+ return html;
+}
+
+function activitiesRenderEditForm(section, mood, entries) {
+ let html = '';
+ for (let i = 0; i < entries.length; i++) {
+ const e = entries[i];
+ html += ``;
+ html += ``;
+ html += ``;
+ html += ``;
+ html += ``;
+ html += `
`;
+ }
+ html += ``;
+ html += ``;
+ html += ``;
+ html += ``;
+ html += `
`;
+ return html;
+}
+
+function activitiesMoodToggle(section, mood) {
+ const el = document.getElementById(`act-content-${section}-${mood}`);
+ const iconEl = document.getElementById(`act-icon-${section}-${mood}`);
+ if (!el) return;
+ const isOpen = el.style.display === 'block';
+ el.style.display = isOpen ? 'none' : 'block';
+ if (iconEl) iconEl.textContent = isOpen ? '▶' : '▼';
+}
+
+function activitiesStartEdit(section, mood) {
+ const key = `${section}/${mood}`;
+ const entries = activitiesData[section][mood];
+ // Deep clone entries for editing
+ activitiesEditCache[key] = JSON.parse(JSON.stringify(entries));
+ activitiesEditing[key] = true;
+ activitiesRenderSection(section);
+ // Auto-expand the mood panel
+ const el = document.getElementById(`act-content-${section}-${mood}`);
+ const iconEl = document.getElementById(`act-icon-${section}-${mood}`);
+ if (el) el.style.display = 'block';
+ if (iconEl) iconEl.textContent = '▼';
+}
+
+function activitiesCancelEdit(section, mood) {
+ const key = `${section}/${mood}`;
+ delete activitiesEditing[key];
+ delete activitiesEditCache[key];
+ activitiesRenderSection(section);
+}
+
+function activitiesAddEntry(section, mood) {
+ const key = `${section}/${mood}`;
+ // First, sync current form values to cache
+ activitiesSyncFormToCache(section, mood);
+ activitiesEditCache[key].push({ type: 'listening', name: '', weight: 1 });
+ activitiesRenderSection(section);
+ // Keep the mood panel open
+ const el = document.getElementById(`act-content-${section}-${mood}`);
+ if (el) el.style.display = 'block';
+}
+
+function activitiesRemoveEntry(section, mood, index) {
+ const key = `${section}/${mood}`;
+ activitiesSyncFormToCache(section, mood);
+ activitiesEditCache[key].splice(index, 1);
+ activitiesRenderSection(section);
+ const el = document.getElementById(`act-content-${section}-${mood}`);
+ if (el) el.style.display = 'block';
+}
+
+function activitiesSyncFormToCache(section, mood) {
+ const key = `${section}/${mood}`;
+ const entries = activitiesEditCache[key] || [];
+ for (let i = 0; i < entries.length; i++) {
+ const typeEl = document.getElementById(`act-type-${section}-${mood}-${i}`);
+ const nameEl = document.getElementById(`act-name-${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 (weightEl) entries[i].weight = parseInt(weightEl.value) || 1;
+ }
+ activitiesEditCache[key] = entries;
+}
+
+async function activitiesSave(section, mood) {
+ const key = `${section}/${mood}`;
+ activitiesSyncFormToCache(section, mood);
+ const entries = activitiesEditCache[key];
+
+ // Client-side validation
+ for (let i = 0; i < entries.length; i++) {
+ if (!entries[i].name || !entries[i].name.trim()) {
+ showNotification(`Entry ${i + 1}: name cannot be empty`, 'error');
+ return;
+ }
+ if (!entries[i].weight || entries[i].weight < 1) {
+ showNotification(`Entry ${i + 1}: weight must be at least 1`, 'error');
+ return;
+ }
+ }
+
+ try {
+ await apiCall(`/activities/${section}/${mood}`, 'POST', { activities: entries });
+ showNotification(`Saved activities for ${section}/${mood}`, 'success');
+ delete activitiesEditing[key];
+ delete activitiesEditCache[key];
+ // Reload to get fresh data
+ await activitiesLoad();
+ } catch (e) {
+ showNotification('Save failed: ' + e.message, 'error');
+ }
+}
+