From 3a71cd3cf239426deffe105d0cbd9273ace4598d Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Sun, 18 Jan 2026 10:32:01 -0500 Subject: [PATCH 01/11] Move immutable input data to the global "insights" store --- .../mixtape/core/insights/insightsTabs.js | 98 +++++-------------- .../mixtape/core/insights/rewardsFrequency.js | 4 +- .../core/insights/rewardsOverTimeDecomp.js | 12 +-- .../mixtape/core/js/alpine_insights_store.js | 58 +++++++++-- .../core/insights/action_v_frequency.html | 6 +- .../insights/action_v_reward_frequency.html | 6 +- .../templates/core/insights/clustering.html | 54 +++------- .../templates/core/insights/episode_info.html | 2 +- .../insights/partials/_tabs_overview.html | 21 ++-- .../core/insights/rewards_over_time.html | 16 +-- .../templates/core/insights/timeline.html | 10 +- 11 files changed, 125 insertions(+), 162 deletions(-) diff --git a/mixtape/core/static/mixtape/core/insights/insightsTabs.js b/mixtape/core/static/mixtape/core/insights/insightsTabs.js index 1329c6e..08841ae 100644 --- a/mixtape/core/static/mixtape/core/insights/insightsTabs.js +++ b/mixtape/core/static/mixtape/core/insights/insightsTabs.js @@ -2,22 +2,8 @@ document.addEventListener('alpine:init', () => { Alpine.data('insightsTabs', () => ({ // initial state active: 'Overview', - parsedData: null, - clustering: null, - episodesCount: null, - agentsCount: null, maxStepsCount: null, avgRewardDisplay: null, - actionVReward: {}, - rewardHistogram: [], - actionVFrequency: {}, - rewardsOverTime: [], - decomposedRewards: [], - uniqueAgents: [], - episodeIds: [], - stepData: [], - timelineKeySteps: [], - timelineKeyStepsGlobal: [], currentStep: 0, maxSteps: 0, maxStepsGlobal: 0, @@ -27,8 +13,6 @@ document.addEventListener('alpine:init', () => { limitToEpisode: false, debug: false, playing: false, - multiKey: '', - clusteringAvailable: false, pollTimer: null, showToast: false, toastText: '', @@ -42,41 +26,15 @@ document.addEventListener('alpine:init', () => { pollBeforeUnloadHandler: null, init() { - // Parse JSON data - this.parsedData = JSON.parse( - document.getElementById('parsed_data_json')?.textContent || 'null' - ); - this.clustering = JSON.parse( - document.getElementById('clustering_json')?.textContent || 'null' - ); - this.multiKey = JSON.parse( - document.getElementById('clustering_multi_key')?.textContent || '""' - ); - this.clusteringAvailable = JSON.parse( - document.getElementById('clustering_available_json')?.textContent || 'false' - ); - this.episodeIds = this.parsedData?.episode_ids ?? []; - this.uniqueAgents = this.parsedData?.unique_agents ?? []; - this.rewardsOverTime = this.parsedData?.rewards_over_time ?? []; - this.decomposedRewards = this.parsedData?.decomposed_rewards ?? []; - this.actionVReward = this.parsedData?.action_v_reward ?? {}; - this.rewardHistogram = this.parsedData?.reward_histogram ?? []; - this.actionVFrequency = this.parsedData?.action_v_frequency ?? {}; - this.stepData = this.parsedData?.step_data ?? []; - this.timelineKeySteps = this.parsedData?.timeline_key_steps ?? []; - this.timelineKeyStepsGlobal = this.parsedData?.timeline_key_steps_global ?? []; - this.episodesCount = Array.isArray(this.episodeIds) ? this.episodeIds.length : null; - this.agentsCount = Array.isArray(this.uniqueAgents) ? this.uniqueAgents.length : null; - this.maxStepsGlobal = (this.parsedData?.max_steps ?? 1) - 1; + this.maxStepsGlobal = (this.$store.insights.parsedData?.max_steps ?? 1) - 1; this.maxSteps = this.maxStepsGlobal; // Average reward - const avgReward = this.computeAverageReward(this.parsedData); + const avgReward = this.computeAverageReward(this.$store.insights.parsedData); this.avgRewardDisplay = (avgReward ?? null) !== null ? Number(avgReward).toFixed(2) : null; // Episode summary - this.episodeSummaries = (Array.isArray(this.episodeIds) ? this.episodeIds : []) - .map((id, idx) => this.summarizeEpisode(idx, id)); + this.episodeSummaries = this.$store.insights.episodeIds.map((id, idx) => this.summarizeEpisode(idx, id)); // Max steps count (for multi-episode mode) this.maxStepsCount = (() => { @@ -111,23 +69,16 @@ document.addEventListener('alpine:init', () => { // Push derived values store.currentStep = this.currentStep; store.episodeSummaries = this.episodeSummaries; - store.multiKey = this.multiKey; - store.clusteringAvailable = this.clusteringAvailable; - store.clustering = this.clustering; } // push changes this.$watch('episodeSummaries', v => { Alpine.store('insights').episodeSummaries = v; }); this.$watch('currentStep', v => { Alpine.store('insights').currentStep = v; }); - this.$watch('clustering', (v) => { - const store = (window.Alpine && Alpine.store('insights')) || null; - if (store) store.clustering = v; - }); // Polling: multi-episode artifact or single-episode partials - const isMulti = Array.isArray(this.episodeIds) && this.episodeIds.length > 1; - const isSingle = Array.isArray(this.episodeIds) && this.episodeIds.length === 1; - if ((isMulti && !this.clustering && this.multiKey) - || (isSingle && (!this.clustering || !this.clustering?.obs || !this.clustering?.agent_outs))) { + const isMulti = this.$store.insights.episodeIds.length > 1; + const isSingle = this.$store.insights.episodeIds.length === 1; + if ((isMulti && !this.$store.insights.clustering && this.$store.insights.multiKey) + || (isSingle && (!this.$store.insights.clustering || !this.$store.insights.clustering?.obs || !this.$store.insights.clustering?.agent_outs))) { this.startClusteringPolling(); } }, @@ -175,7 +126,7 @@ document.addEventListener('alpine:init', () => { // helpers for templates getEpisodeStepIndex(epIdx) { - const steps = this.stepData?.[epIdx] || {}; + const steps = this.$store.insights.stepData?.[epIdx] || {}; const keys = Object.keys(steps); if (!keys.length) return null; const nums = keys.map(k => +k).sort((a,b) => a - b); @@ -186,7 +137,7 @@ document.addEventListener('alpine:init', () => { getEpisodeStepData(epIdx) { const idx = this.getEpisodeStepIndex(epIdx); if (idx === null) return null; - const steps = this.stepData?.[epIdx] || {}; + const steps = this.$store.insights.stepData?.[epIdx] || {}; return steps[idx] ?? steps[String(idx)] ?? null; }, // Compatibility alias used by some templates @@ -251,13 +202,10 @@ document.addEventListener('alpine:init', () => { return null; }, summarizeEpisode(idx, id) { - const allRewards = Array.isArray(this.rewardsOverTime[idx]) ? this.rewardsOverTime[idx] : []; + const allRewards = this.$store.insights.rewardsOverTime[idx]; const steps = allRewards.length || null; const perStep = allRewards.filter(rewards => typeof rewards === 'number'); - let totalReward = null; - if (Array.isArray(this.parsedData?.episode_total_rewards)) { - totalReward = typeof this.parsedData.episode_total_rewards[idx] === 'number' ? this.parsedData.episode_total_rewards[idx] : null; - } + let totalReward = this.$store.insights.parsedData.episode_total_rewards[idx]; if (totalReward === null && allRewards.length) { totalReward = this.totalFromSeries(allRewards); } @@ -297,24 +245,24 @@ document.addEventListener('alpine:init', () => { if (this.pollBeforeUnloadHandler) { window.removeEventListener('beforeunload', this.pollBeforeUnloadHandler); this.pollBeforeUnloadHandler = null; } }, async checkClusteringStatus() { - const isMulti = !!this.multiKey && Array.isArray(this.episodeIds) && this.episodeIds.length > 1; + const isMulti = !!this.$store.insights.multiKey && this.$store.insights.episodeIds.length > 1; if (isMulti) { const resp = await fetch( - `/api/v1/clustering/status/?multi_key=${encodeURIComponent(this.multiKey)}`, + `/api/v1/clustering/status/?multi_key=${encodeURIComponent(this.$store.insights.multiKey)}`, { credentials: 'same-origin' } ); if (!resp.ok) return; const data = await resp.json(); if (data && data.available) { - this.clusteringAvailable = true; + this.$store.insights.clusteringAvailable = true; await this.fetchClusteringResult(); this.stopClusteringPolling(); } return; } - const isSingle = Array.isArray(this.episodeIds) && this.episodeIds.length === 1; + const isSingle = this.$store.insights.episodeIds.length === 1; if (isSingle) { - const eid = this.episodeIds[0]; + const eid = this.$store.insights.episodeIds[0]; const resp = await fetch( `/api/v1/clustering/status/?episode_id=${encodeURIComponent(eid)}`, { credentials: 'same-origin' } @@ -326,21 +274,21 @@ document.addEventListener('alpine:init', () => { await this.fetchClusteringResult(); } if (data.available) { - this.clusteringAvailable = true; + this.$store.insights.clusteringAvailable = true; this.stopClusteringPolling(); } } }, async fetchClusteringResult() { let resp = null; - const isMulti = !!this.multiKey && Array.isArray(this.episodeIds) && this.episodeIds.length > 1; + const isMulti = !!this.$store.insights.multiKey && this.$store.insights.episodeIds.length > 1; if (isMulti) { resp = await fetch( - `/api/v1/clustering/result/?multi_key=${encodeURIComponent(this.multiKey)}`, + `/api/v1/clustering/result/?multi_key=${encodeURIComponent(this.$store.insights.multiKey)}`, { credentials: 'same-origin' } ); - } else if (Array.isArray(this.episodeIds) && this.episodeIds.length === 1) { - const eid = this.episodeIds[0]; + } else if (this.$store.insights.episodeIds.length === 1) { + const eid = this.$store.insights.episodeIds[0]; resp = await fetch( `/api/v1/clustering/result/?episode_id=${encodeURIComponent(eid)}`, { credentials: 'same-origin' } @@ -348,10 +296,10 @@ document.addEventListener('alpine:init', () => { } if (!resp || !resp.ok) return; const obj = await resp.json(); - this.clustering = obj; + this.$store.insights.clustering = obj; const store = (window.Alpine && Alpine.store('insights')) || null; if (store) store.clustering = obj; - if (this.clustering && ((this.clustering.obs && this.clustering.agent_outs) || isMulti)) { + if (this.$store.insights.clustering && ((this.$store.insights.clustering.obs && this.$store.insights.clustering.agent_outs) || isMulti)) { this.toastText = 'Clustering computation completed'; this.showToast = true; if (this.toastTimer) { clearTimeout(this.toastTimer); } diff --git a/mixtape/core/static/mixtape/core/insights/rewardsFrequency.js b/mixtape/core/static/mixtape/core/insights/rewardsFrequency.js index bf751bd..27d6aa9 100644 --- a/mixtape/core/static/mixtape/core/insights/rewardsFrequency.js +++ b/mixtape/core/static/mixtape/core/insights/rewardsFrequency.js @@ -29,11 +29,11 @@ document.addEventListener('alpine:init', () => { }, plot: null, init() { - this.rewardHistogram.forEach((episode, idx) => { + this.$store.insights.rewardHistogram.forEach((episode, idx) => { this.data.push({ x: episode, type: 'histogram', - name: `Episode ${this.episodeIds[idx]}`, + name: `Episode ${this.$store.insights.episodeIds[idx]}`, marker: { line: { width: 2 diff --git a/mixtape/core/static/mixtape/core/insights/rewardsOverTimeDecomp.js b/mixtape/core/static/mixtape/core/insights/rewardsOverTimeDecomp.js index 8736acf..4d76ec7 100644 --- a/mixtape/core/static/mixtape/core/insights/rewardsOverTimeDecomp.js +++ b/mixtape/core/static/mixtape/core/insights/rewardsOverTimeDecomp.js @@ -46,7 +46,7 @@ document.addEventListener('alpine:init', () => { }, initPlot() { // Plot decomposed rewards for each episode - this.decomposedRewards.forEach((episodeRewards, episodeIdx) => { + this.$store.insights.decomposedRewards.forEach((episodeRewards, episodeIdx) => { Object.keys(episodeRewards).forEach((rewardType, rewardIdx) => { const rewardData = episodeRewards[rewardType]; this.data.push({ @@ -54,13 +54,13 @@ document.addEventListener('alpine:init', () => { y: rewardData, type: 'scatter', mode: 'lines', - name: `Episode ${this.episodeIds[episodeIdx]} - ${rewardType}`, + name: `Episode ${this.$store.insights.episodeIds[episodeIdx]} - ${rewardType}`, }); }); }); // Calculate overall min and max Y values from all data - const allYValues = this.decomposedRewards.flatMap(episodeRewards => + const allYValues = this.$store.insights.decomposedRewards.flatMap(episodeRewards => Object.values(episodeRewards).flatMap(rewardData => rewardData) ); @@ -94,8 +94,8 @@ document.addEventListener('alpine:init', () => { }]; }, createAnnotations() { - const episodeCount = Object.keys(this.decomposedRewards).length; - const annotations = this.decomposedRewards.flatMap((episodeRewards, episodeIdx) => { + const episodeCount = Object.keys(this.$store.insights.decomposedRewards).length; + const annotations = this.$store.insights.decomposedRewards.flatMap((episodeRewards, episodeIdx) => { const componentCount = Object.keys(episodeRewards).length; const offset = 40 / (componentCount * episodeCount); return Object.entries(episodeRewards).map(([rewardType, rewardData]) => { @@ -124,7 +124,7 @@ document.addEventListener('alpine:init', () => { // Calculate overall min and max Y values from all data let allYValues = []; // Get all Y values from decomposed rewards - this.decomposedRewards.forEach(episodeRewards => { + this.$store.insights.decomposedRewards.forEach(episodeRewards => { Object.values(episodeRewards).forEach(rewardData => { allYValues = allYValues.concat(rewardData); }); diff --git a/mixtape/core/static/mixtape/core/js/alpine_insights_store.js b/mixtape/core/static/mixtape/core/js/alpine_insights_store.js index c81abf6..5800911 100644 --- a/mixtape/core/static/mixtape/core/js/alpine_insights_store.js +++ b/mixtape/core/static/mixtape/core/js/alpine_insights_store.js @@ -1,10 +1,52 @@ document.addEventListener('alpine:init', () => { - Alpine.store('insights', - { - // User-configurable flags - episodeSummaries: [], - // Runtime values - currentStep: 0, - } - ); + Alpine.store('insights', { + // Immutable data + parsedData: null, + clustering: null, + multiKey: '', + clusteringAvailable: false, + + episodeIds: [], + uniqueAgents: [], + rewardsOverTime: [], + decomposedRewards: [], + actionVReward: {}, + rewardHistogram: [], + actionVFrequency: {}, + stepData: [], + timelineKeySteps: [], + timelineKeyStepsGlobal: [], + + // User-configurable flags + episodeSummaries: [], + // Runtime values + currentStep: 0, + + init() { + // Parse JSON data + this.parsedData = JSON.parse( + document.getElementById('parsed_data_json').textContent + ); + this.clustering = JSON.parse( + document.getElementById('clustering_json').textContent + ); + this.multiKey = JSON.parse( + document.getElementById('clustering_multi_key').textContent + ); + this.clusteringAvailable = JSON.parse( + document.getElementById('clustering_available_json').textContent + ); + + this.episodeIds = this.parsedData.episode_ids; + this.uniqueAgents = this.parsedData.unique_agents; + this.rewardsOverTime = this.parsedData.rewards_over_time; + this.decomposedRewards = this.parsedData?.decomposed_rewards ?? []; + this.actionVReward = this.parsedData?.action_v_reward ?? {}; + this.rewardHistogram = this.parsedData?.reward_histogram ?? []; + this.actionVFrequency = this.parsedData?.action_v_frequency ?? {}; + this.stepData = this.parsedData?.step_data ?? []; + this.timelineKeySteps = this.parsedData?.timeline_key_steps ?? []; + this.timelineKeyStepsGlobal = this.parsedData?.timeline_key_steps_global ?? []; + }, + }); }); diff --git a/mixtape/core/templates/core/insights/action_v_frequency.html b/mixtape/core/templates/core/insights/action_v_frequency.html index d3b8d1a..bf4d820 100644 --- a/mixtape/core/templates/core/insights/action_v_frequency.html +++ b/mixtape/core/templates/core/insights/action_v_frequency.html @@ -3,7 +3,7 @@ layout: { title: {text: 'Actions VS Frequency'}, autosize: true, - barmode: Object.keys(actionVFrequency).length > 1 ? 'group' : 'stack', + barmode: Object.keys($store.insights.actionVFrequency).length > 1 ? 'group' : 'stack', showlegend: $store.settings.showPlotLegends, paper_bgcolor: $store.theme.backgroundColor, plot_bgcolor: $store.theme.backgroundColor, @@ -25,14 +25,14 @@ displayModeBar: false, }, plot: null, - actionVFrequency: Object.keys(actionVFrequency).length > 1 ? actionVFrequency : Object.values(actionVFrequency)[0], + actionVFrequency: Object.keys($store.insights.actionVFrequency).length > 1 ? $store.insights.actionVFrequency : Object.values($store.insights.actionVFrequency)[0], resizePlot: _.debounce(function() { if (!this.$refs.actionsFrequency.querySelector('.plotly')) return; Plotly.Plots.resize(this.$refs.actionsFrequency); }, 200, {leading: true}), }" x-init=" - Object.entries(actionVFrequency).forEach(([grouping, values]) => { + Object.entries($store.insights.actionVFrequency).forEach(([grouping, values]) => { data.push({ x: Object.keys(values), y: Object.values(values), diff --git a/mixtape/core/templates/core/insights/action_v_reward_frequency.html b/mixtape/core/templates/core/insights/action_v_reward_frequency.html index 0a94be1..87db1e7 100644 --- a/mixtape/core/templates/core/insights/action_v_reward_frequency.html +++ b/mixtape/core/templates/core/insights/action_v_reward_frequency.html @@ -15,7 +15,7 @@ zeroline: false, }, autosize: true, - barmode: Object.keys(actionVReward).length > 1 ? 'group' : 'stack', + barmode: Object.keys($store.insights.actionVReward).length > 1 ? 'group' : 'stack', showlegend: $store.settings.showPlotLegends, paper_bgcolor: $store.theme.backgroundColor, plot_bgcolor: $store.theme.backgroundColor, @@ -27,14 +27,14 @@ displayModeBar: false, }, plot: null, - actionVReward: Object.keys(actionVReward).length > 1 ? actionVReward : Object.values(actionVReward)[0], + actionVReward: Object.keys($store.insights.actionVReward).length > 1 ? $store.insights.actionVReward : Object.values($store.insights.actionVReward)[0], resizePlot: _.debounce(function() { if (!this.$refs.actionsRewards.querySelector('.plotly')) return; Plotly.Plots.resize(this.$refs.actionsRewards); }, 200, {leading: true}), }" x-init=" - data = Object.entries(actionVReward).map(([grouping, values]) => ({ + data = Object.entries($store.insights.actionVReward).map(([grouping, values]) => ({ x: Object.keys(values), y: Object.values(values), type: 'bar', diff --git a/mixtape/core/templates/core/insights/clustering.html b/mixtape/core/templates/core/insights/clustering.html index 8bd14da..680de61 100644 --- a/mixtape/core/templates/core/insights/clustering.html +++ b/mixtape/core/templates/core/insights/clustering.html @@ -27,21 +27,15 @@ }, 200, {leading: true}), buildPlot() { this.data = []; - let agents = []; - if ($store.insights && Array.isArray($store.insights.uniqueAgents)) { - agents = $store.insights.uniqueAgents - }; - let episodes = []; - if ($store.insights && Array.isArray($store.insights.episodeIds)) { - episodes = $store.insights.episodeIds - }; + let agents = $store.insights.uniqueAgents; + let episodes = $store.insights.episodeIds; let clustering = null; - if ($store.insights && $store.insights.clustering) { + if ($store.insights.clustering) { clustering = $store.insights.clustering }; if (!clustering || !clustering[{{key}}]) return; for (let agentIdx = 0; agentIdx < agents.length; agentIdx++) { - clustering[{{key}}].episodeManifolds.forEach((episodeManifold, episodeIdx) => { + clustering[{{key}}].episode_manifolds.forEach((episodeManifold, episodeIdx) => { const xCoords = []; const yCoords = []; episodeManifold.forEach(row => { @@ -93,32 +87,22 @@ }, }" x-init=" - if ($store.insights && $store.insights.clustering && $store.insights.clustering[{{key}}]) { + if ($store.insights.clustering && $store.insights.clustering[{{key}}]) { buildPlot(); }; $watch( - () => $store.insights && $store.insights.clustering && $store.insights.clustering[{{key}}], + () => $store.insights.clustering && $store.insights.clustering[{{key}}], (v) => { if (v) { buildPlot(); } } ); " x-effect="() => { - if (!this.plot - && $store.insights - && $store.insights.clustering && $store.insights.clustering[{{key}}] - ) { buildPlot(); } + if (!this.plot && $store.insights.clustering && $store.insights.clustering[{{key}}]) { + buildPlot(); + } if (this.plot) { - let numAgents = 0; - if ($store.insights && Array.isArray($store.insights.uniqueAgents)) { - numAgents = $store.insights.uniqueAgents.length; - } - let numEpisodes = 0; - if ($store.insights && Array.isArray($store.insights.episodeIds)) { - numEpisodes = $store.insights.episodeIds.length; - } - let currentStep = 0; - if ($store.insights && typeof $store.insights.currentStep === 'number') { - currentStep = $store.insights.currentStep; - } + let numAgents = $store.insights.uniqueAgents.length; + let numEpisodes = $store.insights.episodeIds.length; + let currentStep = $store.insights.currentStep; // Get the current plot data const plotElement = $refs.allManifolds; @@ -145,14 +129,8 @@ ) { highlightX.push(agentTrace.x[currentStep]); highlightY.push(agentTrace.y[currentStep]); - let agents = []; - if ($store.insights && Array.isArray($store.insights.uniqueAgents)) { - agents = $store.insights.uniqueAgents; - } - let episodes = []; - if ($store.insights && Array.isArray($store.insights.episodeIds)) { - episodes = $store.insights.episodeIds; - } + let agents = $store.insights.uniqueAgents; + let episodes = $store.insights.episodeIds; highlightText.push(`${agents[agentIdx]} Episode ${episodes[episodeIdx]}`); } } @@ -200,13 +178,13 @@ >
diff --git a/mixtape/core/templates/core/insights/episode_info.html b/mixtape/core/templates/core/insights/episode_info.html index a107eda..8bac15c 100644 --- a/mixtape/core/templates/core/insights/episode_info.html +++ b/mixtape/core/templates/core/insights/episode_info.html @@ -21,7 +21,7 @@ -