diff --git a/mixtape/core/static/mixtape/core/js/feedback_modal.js b/mixtape/core/static/mixtape/core/feedbackModal.js similarity index 100% rename from mixtape/core/static/mixtape/core/js/feedback_modal.js rename to mixtape/core/static/mixtape/core/feedbackModal.js diff --git a/mixtape/core/static/mixtape/core/insights/actionsFrequency.js b/mixtape/core/static/mixtape/core/insights/actionsFrequency.js new file mode 100644 index 0000000..d6d8e8a --- /dev/null +++ b/mixtape/core/static/mixtape/core/insights/actionsFrequency.js @@ -0,0 +1,61 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('actionsFrequency', () => ({ + data: [], + layout: { + title: {text: 'Actions VS Frequency'}, + autosize: true, + barmode: Object.keys(Alpine.store('insights').actionVFrequency).length > 1 ? 'group' : 'stack', + showlegend: Alpine.store('settings').showPlotLegends, + paper_bgcolor: Alpine.store('theme').backgroundColor, + plot_bgcolor: Alpine.store('theme').backgroundColor, + font: { + color: Alpine.store('theme').fontColor + }, + xaxis: { + gridcolor: Alpine.store('theme').gridColor, + linecolor: Alpine.store('theme').gridColor, + zeroline: false, + }, + yaxis: { + gridcolor: Alpine.store('theme').gridColor, + linecolor: Alpine.store('theme').gridColor, + zeroline: false, + }, + }, + config: { + displayModeBar: false, + }, + plot: null, + actionVFrequency: Object.keys(Alpine.store('insights').actionVFrequency).length > 1 ? Alpine.store('insights').actionVFrequency : Object.values(Alpine.store('insights').actionVFrequency)[0], + init() { + Object.entries(this.$store.insights.actionVFrequency).forEach(([grouping, values]) => { + this.data.push({ + x: Object.keys(values), + y: Object.values(values), + type: 'bar', + name: grouping, + marker: { + line: { + width: 2 + } + } + }); + }); + this.plot = Plotly.react(this.$el, this.data, this.layout, this.config); + this.$watch('$store.settings.showPlotLegends', () => { + this.$nextTick(() => { + Plotly.relayout( + this.$el, + { + autosize: true, + showlegend: this.$store.settings.showPlotLegends + }) + }); + }); + }, + resizePlot: _.debounce(function () { + if (!this.$el.querySelector('.plotly')) return; + Plotly.Plots.resize(this.$el); + }, 200, {leading: true}), + })); +}); diff --git a/mixtape/core/static/mixtape/core/insights/actionsRewards.js b/mixtape/core/static/mixtape/core/insights/actionsRewards.js new file mode 100644 index 0000000..4c853f8 --- /dev/null +++ b/mixtape/core/static/mixtape/core/insights/actionsRewards.js @@ -0,0 +1,60 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('actionsRewards', () => ({ + data: [], + layout: { + title: {text: 'Actions VS Reward Frequency'}, + xaxis: { + title: {text: 'Action'}, + gridcolor: Alpine.store('theme').gridColor, + linecolor: Alpine.store('theme').gridColor, + zeroline: false, + }, + yaxis: { + title: {text: 'Reward Frequency'}, + gridcolor: Alpine.store('theme').gridColor, + linecolor: Alpine.store('theme').gridColor, + zeroline: false, + }, + autosize: true, + barmode: Object.keys(Alpine.store('insights').actionVReward).length > 1 ? 'group' : 'stack', + showlegend: Alpine.store('settings').showPlotLegends, + paper_bgcolor: Alpine.store('theme').backgroundColor, + plot_bgcolor: Alpine.store('theme').backgroundColor, + font: { + color: Alpine.store('theme').fontColor + }, + }, + config: { + displayModeBar: false, + }, + plot: null, + actionVReward: Object.keys(Alpine.store('insights').actionVReward).length > 1 ? Alpine.store('insights').actionVReward : Object.values(Alpine.store('insights').actionVReward)[0], + init() { + this.data = Object.entries(this.$store.insights.actionVReward).map(([grouping, values]) => ({ + x: Object.keys(values), + y: Object.values(values), + type: 'bar', + name: grouping, + marker: { + line: { + width: 2 + } + } + })); + this.plot = Plotly.react(this.$el, this.data, this.layout, this.config); + this.$watch('$store.settings.showPlotLegends', () => { + this.$nextTick(() => { + Plotly.relayout( + this.$el, { + autosize: true, + showlegend: this.$store.settings.showPlotLegends + }) + }); + }); + }, + resizePlot: _.debounce(function() { + if (!this.$el.querySelector('.plotly')) return; + Plotly.Plots.resize(this.$el); + }, 200, {leading: true}), + })); +}); diff --git a/mixtape/core/static/mixtape/core/insights/clustering.js b/mixtape/core/static/mixtape/core/insights/clustering.js new file mode 100644 index 0000000..eac8aa2 --- /dev/null +++ b/mixtape/core/static/mixtape/core/insights/clustering.js @@ -0,0 +1,177 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('clustering', (key, title) => ({ + data: [], + layout: { + title: {text: `Agent ${title}`}, + height: 450, + xaxis: { + gridcolor: Alpine.store('theme').gridColor, + linecolor: Alpine.store('theme').gridColor, + zeroline: false, + }, + yaxis: { + gridcolor: Alpine.store('theme').gridColor, + linecolor: Alpine.store('theme').gridColor, + zeroline: false, + }, + paper_bgcolor: Alpine.store('theme').backgroundColor, + plot_bgcolor: Alpine.store('theme').backgroundColor, + font: { + color: Alpine.store('theme').fontColor + }, + }, + plot: null, + init() { + if (this.$store.insights.clustering && this.$store.insights.clustering[key]) { + this.buildPlot(); + } + this.$watch( + () => this.$store.insights.clustering && this.$store.insights.clustering[key], + (v) => { if (v) { this.buildPlot(); } } + ); + Alpine.effect(() => { + if (!this.plot && this.$store.insights.clustering && this.$store.insights.clustering[key]) { + this.buildPlot(); + } + if (this.plot) { + const numAgents = this.$store.insights.uniqueAgents.length; + const numEpisodes = this.$store.insights.episodeIds.length; + const currentStep = this.$store.insights.currentStep; + + // Get the current plot data + const plotElement = this.$el; + const currentData = plotElement.data; + + // Remove the highlight trace if it exists (it will be last, at numEpisodes * numAgents + 1) + if (currentData && currentData.length > (numEpisodes * numAgents + 1)) { + Plotly.deleteTraces(this.$el, [numEpisodes * numAgents + 1]); + } + + // Create arrays for the highlighted points + const highlightX = []; + const highlightY = []; + const highlightText = []; + + for (let agentIdx = 0; agentIdx < numAgents; agentIdx++) { + for (let episodeIdx = 0; episodeIdx < numEpisodes; episodeIdx++) { + const idx = episodeIdx + agentIdx * numEpisodes; + const agentTrace = currentData && currentData[idx]; + if (agentTrace + && Array.isArray(agentTrace.x) + && currentStep < agentTrace.x.length + && (!agentTrace.visible || agentTrace.visible === true) + ) { + highlightX.push(agentTrace.x[currentStep]); + highlightY.push(agentTrace.y[currentStep]); + const agents = this.$store.insights.uniqueAgents; + const episodes = this.$store.insights.episodeIds; + highlightText.push(`${agents[agentIdx]} Episode ${episodes[episodeIdx]}`); + } + } + } + + // Only add the highlight trace if there are visible points + if (highlightX.length > 0) { + // Add new trace for highlighted points + const textPosition = [ + 'top left', + 'middle left', + 'bottom left', + 'top center', + 'middle center', + 'bottom center', + 'top right', + 'middle right', + 'bottom right' + ]; + Plotly.addTraces(this.$el, [{ + x: highlightX, + y: highlightY, + text: highlightText, + type: 'scatter', + mode: 'markers+text', + showlegend: false, + textposition: textPosition, + textfont: { + size: 12, + color: this.$store.theme.fontColor + }, + marker: { + color: 'transparent', + size: 20, + line: { + width: 3, + color: 'red' + } + } + }]); + } + } + }); + }, + resizePlot: _.debounce(function() { + if (!this.$el.querySelector('.plotly')) return; + Plotly.Plots.resize(this.$el); + }, 200, {leading: true}), + buildPlot() { + this.data = []; + const agents = this.$store.insights.uniqueAgents; + const episodes = this.$store.insights.episodeIds; + let clustering = null; + if (this.$store.insights.clustering) { + clustering = this.$store.insights.clustering + } + if (!clustering || !clustering[key]) return; + for (let agentIdx = 0; agentIdx < agents.length; agentIdx++) { + clustering[key].episode_manifolds.forEach((episodeManifold, episodeIdx) => { + const xCoords = []; + const yCoords = []; + episodeManifold.forEach(row => { + const embedding = Array.isArray(row) ? row[agentIdx] : null; + if (embedding && Array.isArray(embedding) && embedding.length >= 2) { + xCoords.push(embedding[0]); yCoords.push(embedding[1]); + } + }); + this.data.push({ + x: xCoords, + y: yCoords, + type: 'scatter', + mode: 'lines', + colorscale: 'YlOrRd', + line: { opacity: 1, width: 2 }, + name: `Episode ${episodes[episodeIdx]} ${agents[agentIdx]}` + }); + }); + } + this.data.push({ + x: clustering[key].all_manifolds_x, + y: clustering[key].all_manifolds_y, + type: 'scatter', + mode: 'markers', + marker: { + color: clustering[key].all_clusters, + cmin: 0, + cmax: 9, + colorscale: 'Viridis', + size: 20, + opacity: Array(clustering[key].all_manifolds_x.length).fill(1) + }, + name: 'All Episodes' + }); + this.plot = Plotly.newPlot( + this.$el, + this.data, + this.layout, + { displayModeBar: false, responsive: true } + ); + this.$watch('$store.settings.showPlotLegends', () => { + this.$nextTick(() => { + Plotly.relayout(this.$el, { + autosize: true, + showlegend: this.$store.settings.showPlotLegends, + }) + }); + }); + }, + })); +}); diff --git a/mixtape/core/static/mixtape/core/insights/clusteringTimeline.js b/mixtape/core/static/mixtape/core/insights/clusteringTimeline.js new file mode 100644 index 0000000..4a926b3 --- /dev/null +++ b/mixtape/core/static/mixtape/core/insights/clusteringTimeline.js @@ -0,0 +1,139 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('clusteringTimeline', (key, title) => ({ + data: [], + layout: { + title: {text: `Agent ${title} Timeline`}, + height: 300, + paper_bgcolor: Alpine.store('theme').backgroundColor, + plot_bgcolor: Alpine.store('theme').backgroundColor, + font: { + color: Alpine.store('theme').fontColor + }, + }, + plot: null, + init() { + if (this.$store.insights && this.$store.insights.clustering && this.$store.insights.clustering[key]) { + this.buildPlot(); + } + this.$watch( + '$store.settings.timelineEpisodeIdx', + () => { + if (this.$store.insights && this.$store.insights.clustering && this.$store.insights.clustering[key]) { + this.buildPlot(); + } + } + ); + this.$watch( + () => this.$store.insights && this.$store.insights.clustering && this.$store.insights.clustering[key], + (v) => { + if (v) { + this.buildPlot(); + } + } + ); + Alpine.effect(() => { + if ( + !this.plot + && this.$store.insights + && this.$store.insights.clustering && this.$store.insights.clustering[key] + ) { this.buildPlot(); } + if (this.plot) { + const plotElement = this.$el; + const traces = plotElement && plotElement.data ? plotElement.data : null; + if (!traces || traces.length < 2) return; + let clustering = null; + if (this.$store.insights && this.$store.insights.clustering) { + clustering = this.$store.insights.clustering; + } + if (!clustering) return; + const epClusters = clustering[key]?.episode_clusters; + if (!epClusters) return; + const epIdx = this.$store.settings.timelineEpisodeIdx; + const zNow = Array.isArray(epClusters?.[0]?.[0]) + ? (epClusters?.[epIdx] || epClusters?.[0] || []) + : epClusters; + if (!Array.isArray(zNow) || zNow.length === 0) { return; } + const agentsNow = zNow.length; + const yMin = 0; + const yMax = Math.max(0, (agentsNow || 1) - 1); + const step = this.$store.insights.currentStep; + const xLine = [step, step]; + const yLine = [yMin - 0.5, yMax + 0.5]; + // Use restyle: arguments must be arrays-of-arrays per selected trace + Plotly.restyle(this.$el, { x: [xLine], y: [yLine] }, [1]); + } + }); + }, + resizePlot: _.debounce(function() { + if (!this.$el.querySelector('.plotly')) return; + Plotly.Plots.resize(this.$el); + }, 200, {leading: true}), + buildPlot() { + let clustering = null; + if (this.$store.insights && this.$store.insights.clustering) { + clustering = this.$store.insights.clustering; + } + if (!clustering || !clustering[key]) return; + const epClusters = clustering[key].episode_clusters; + const epIdx = this.$store.settings.timelineEpisodeIdx; + // If multi-episode shape [episodes][agents][steps], pick selected episode; else assume [agents][steps] + let agentStepClusters = Array.isArray(epClusters?.[0]?.[0]) + ? (epClusters?.[epIdx] || epClusters?.[0] || []) + : epClusters; + // Pad each agent row to equal length [agents][steps] + const maxLen = Array.isArray(agentStepClusters) + ? Math.max( + ...agentStepClusters.map(r => (Array.isArray(r) ? r.length : 0)) + ) + : 0; + agentStepClusters = Array.isArray(agentStepClusters) + ? agentStepClusters.map(r => { + const row = Array.isArray(r) ? r.slice() : []; + while (row.length < maxLen) row.push(null); + return row; + }) + : agentStepClusters; + const currentStepVal = this.$store.insights.currentStep; + const yVal = Array.isArray(agentStepClusters) + ? agentStepClusters.length - 1 + : 1; + this.data = [ + { + agentStepClusters: agentStepClusters, + type: 'heatmap', + showscale: false, + colorscale: 'YlGnBu', + }, + { + x: [currentStepVal, currentStepVal], + y: [0, yVal], + type: 'scatter', + mode: 'lines', + showlegend: false, + line: { color: 'red', width: 4, opacity: 0.5 }, + } + ]; + this.plot = Plotly.newPlot( + this.$el, + this.data, + this.layout, + { + displayModeBar: false, + responsive: true + } + ); + this.$watch('$store.settings.showPlotLegends', () => { + this.$nextTick(() => { + Plotly.relayout( + this.$el, + { + autosize: true, + showlegend: this.$store.settings.showPlotLegends + } + ); + }); + }); + }, + + })); +}); diff --git a/mixtape/core/static/mixtape/core/insights/insightsStore.js b/mixtape/core/static/mixtape/core/insights/insightsStore.js new file mode 100644 index 0000000..01cb858 --- /dev/null +++ b/mixtape/core/static/mixtape/core/insights/insightsStore.js @@ -0,0 +1,50 @@ +document.addEventListener('alpine:init', () => { + Alpine.store('insights', { + // Immutable data + parsedData: null, + clustering: null, + multiKey: '', + clusteringAvailable: false, + + episodeIds: [], + uniqueAgents: [], + rewardsOverTime: [], + decomposedRewards: [], + actionVReward: {}, + rewardHistogram: [], + actionVFrequency: {}, + stepData: [], + timelineKeySteps: [], + + // 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; + }, + }); +}); diff --git a/mixtape/core/static/mixtape/core/insights/insightsTabs.js b/mixtape/core/static/mixtape/core/insights/insightsTabs.js index 1329c6e..009a689 100644 --- a/mixtape/core/static/mixtape/core/insights/insightsTabs.js +++ b/mixtape/core/static/mixtape/core/insights/insightsTabs.js @@ -2,33 +2,11 @@ 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, episodeSummaries: [], mediaEpisodeIdx: 0, linkMediaToEpisode: true, - limitToEpisode: false, - debug: false, - playing: false, - multiKey: '', - clusteringAvailable: false, pollTimer: null, showToast: false, toastText: '', @@ -42,41 +20,12 @@ 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.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 = (() => { @@ -88,10 +37,7 @@ document.addEventListener('alpine:init', () => { })(); // Build watchers - this.updateMaxSteps(); - this.$watch('limitToEpisode', () => this.updateMaxSteps()); this.$watch('$store.settings.timelineEpisodeIdx', () => { - this.updateMaxSteps(); if (this.linkMediaToEpisode) this.mediaEpisodeIdx = this.$store.settings.timelineEpisodeIdx; }); this.$watch('episodeSummaries', () => { @@ -102,49 +48,24 @@ document.addEventListener('alpine:init', () => { } }); this.$watch('linkMediaToEpisode', (v) => { if (v) { this.mediaEpisodeIdx = this.$store.settings.timelineEpisodeIdx; } }); - // Ensure timeline mode changes recompute limits - this.$watch('$store.settings.useGlobalTimeline', this.updateMaxSteps); // Store const store = (window.Alpine && Alpine.store('insights')) || null; if (store) { // 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(); } }, - - stepForward() { - if (!this.playing) return; - if (this.currentStep < this.maxSteps) { - const delay = Math.max(50, Math.round(500 / (this.$store.settings.playbackSpeed || 1))); - setTimeout(() => { this.currentStep++; this.stepForward(); }, delay); - } else { this.playing = false; this.currentStep = 0; } - }, - updateMaxSteps() { - if (this.limitToEpisode) { - const steps = this.episodeSummaries[this.$store.settings.timelineEpisodeIdx]?.steps ?? null; - this.maxSteps = (typeof steps === 'number' && steps > 0) ? steps - 1 : this.maxStepsGlobal; - } else { this.maxSteps = this.maxStepsGlobal; } - }, selectTab(tab) { this.active = tab; }, @@ -158,35 +79,20 @@ document.addEventListener('alpine:init', () => { .map((episodeSummary) => `Ep ${episodeSummary.id}: ${episodeSummary.steps ?? '-'}`) ].join('\n'); }, - timelineTitle(ts) { - if (Array.isArray(ts?.episodes)) { - const parts = ts.episodes.map(e => `Episode ${e.episode_id}: ${Number(e.total_rewards ?? 0).toFixed(2)}`); - return `Step ${ts.number}\n${parts.join('\n')}`; - } - return `Step ${ts?.number} — Reward: ${Number((ts && ts.total_rewards) ?? 0).toFixed(2)}`; - }, - timelineAria(ts) { - if (Array.isArray(ts?.episodes)) { - const parts = ts.episodes.map(e => `Episode ${e.episode_id}: ${Number(e.total_rewards ?? 0).toFixed(2)}`); - return `Step ${ts.number} ${parts.join(', ')}`; - } - return `Step ${ts?.number} Reward ${Number((ts && ts.total_rewards) ?? 0).toFixed(2)}`; - }, - // 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); let chosen = nums[0]; - for (let i = 0; i < nums.length; i++) { if (nums[i] <= this.currentStep) chosen = nums[i]; else break; } + for (let i = 0; i < nums.length; i++) { if (nums[i] <= this.$store.insights.currentStep) chosen = nums[i]; else break; } return chosen; }, 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 +157,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 +200,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 +229,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 +251,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/replay.js b/mixtape/core/static/mixtape/core/insights/replay.js new file mode 100644 index 0000000..115956a --- /dev/null +++ b/mixtape/core/static/mixtape/core/insights/replay.js @@ -0,0 +1,21 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('replay', () => ({ + playing: false, + maxSteps: Alpine.store('insights').parsedData.max_steps - 1, + stepForward() { + if (!this.playing) { + return; + } + if (this.$store.insights.currentStep < this.maxSteps) { + const delay = Math.max(50, Math.round(500 / (this.$store.settings.playbackSpeed || 1))); + setTimeout(() => { + this.$store.insights.currentStep++; + this.stepForward(); + }, delay); + } else { + this.playing = false; + this.$store.insights.currentStep = 0; + } + }, + })); +}); diff --git a/mixtape/core/static/mixtape/core/insights/rewardsFrequency.js b/mixtape/core/static/mixtape/core/insights/rewardsFrequency.js index bf751bd..88be9b1 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 @@ -41,11 +41,11 @@ document.addEventListener('alpine:init', () => { } }); }); - this.plot = Plotly.newPlot(this.$refs.rewardsFrequency, this.data, this.layout, this.config); + this.plot = Plotly.newPlot(this.$el, this.data, this.layout, this.config); this.$watch('$store.settings.showPlotLegends', () => { this.$nextTick(() => { Plotly.relayout( - this.$refs.rewardsFrequency, + this.$el, { autosize: true, showlegend: this.$store.settings.showPlotLegends @@ -54,8 +54,8 @@ document.addEventListener('alpine:init', () => { }); }, resizePlot: _.debounce(function() { - if (!this.$refs.rewardsFrequency.querySelector('.plotly')) return; - Plotly.Plots.resize(this.$refs.rewardsFrequency); + if (!this.$el.querySelector('.plotly')) return; + Plotly.Plots.resize(this.$el); }, 200, {leading: true}), })); }); diff --git a/mixtape/core/static/mixtape/core/insights/rewardsOverTime.js b/mixtape/core/static/mixtape/core/insights/rewardsOverTime.js new file mode 100644 index 0000000..18dac1f --- /dev/null +++ b/mixtape/core/static/mixtape/core/insights/rewardsOverTime.js @@ -0,0 +1,105 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('rewardsOverTime', () => ({ + data: [], + layout: { + title: {text: 'Rewards Over Time'}, + xaxis: { + title: {text: 'Time Step'}, + gridcolor: Alpine.store('theme').gridColor, + linecolor: Alpine.store('theme').gridColor, + zeroline: false, + }, + yaxis: { + title: {text: 'Cumulative Reward'}, + gridcolor: Alpine.store('theme').gridColor, + linecolor: Alpine.store('theme').gridColor, + zeroline: false, + }, + autosize: true, + showlegend: Alpine.store('settings').showPlotLegends, + paper_bgcolor: Alpine.store('theme').backgroundColor, + plot_bgcolor: Alpine.store('theme').backgroundColor, + font: { + color: Alpine.store('theme').fontColor + }, + annotations: [ + { + x: Alpine.store('insights').currentStep, + y: Alpine.store('insights').rewardsOverTime[0][Alpine.store('insights').currentStep], + text: Alpine.store('insights').rewardsOverTime[0][Alpine.store('insights').currentStep].toFixed(2), + } + ], + }, + config: { + displayModeBar: false, + responsive: true + }, + plot: null, + init() { + this.$store.insights.rewardsOverTime.forEach((episode, idx) => { + this.data.push({ + x: Array.from({length: episode.length}, (_, i) => i), + y: episode, + type: 'scatter', + name: `Episode ${this.$store.insights.episodeIds[idx]}`, + marker: { + line: { + width: 2 + } + } + }); + }); + this.data.push({ + x: [this.$store.insights.currentStep, this.$store.insights.currentStep], + y: [0, Math.max(...this.$store.insights.rewardsOverTime.flat())], + type: 'scatter', + mode: 'lines', + line: { + color: 'red', + width: 2, + dash: 'dash' + }, + name: 'Current Step' + }); + this.plot = Plotly.newPlot(this.$el, this.data, this.layout, this.config); + this.$watch('$store.settings.showPlotLegends', () => { + this.$nextTick(() => { + Plotly.relayout( + this.$el, + { + autosize: true, + showlegend: this.$store.settings.showPlotLegends + }) + }); + }); + Alpine.effect(() => { + if (this.plot) { + Plotly.update(this.$el, { + x: [[this.$store.insights.currentStep, this.$store.insights.currentStep]], + y: [[Math.min(...this.$store.insights.rewardsOverTime.flat()), Math.max(...this.$store.insights.rewardsOverTime.flat())]] + }, { + annotations: this.$store.insights.rewardsOverTime.map((episode, idx) => { + return { + x: this.getStep(episode), + y: episode[this.getStep(episode)], + text: episode[this.getStep(episode)].toFixed(2), + } + }) + }, [this.$store.insights.rewardsOverTime.length]); + } + }); + }, + resizePlot: _.debounce(function() { + if (!this.$el.querySelector('.plotly')) return; + Plotly.Plots.resize(this.$el); + }, 200, {leading: true}), + getStep(episode) { + if (this.$store.insights.currentStep < Object.keys(episode).length) { + return this.$store.insights.currentStep; + } + return Object.keys(episode).length - 1; + }, + + + })); +}); diff --git a/mixtape/core/static/mixtape/core/insights/rewardsOverTimeDecomp.js b/mixtape/core/static/mixtape/core/insights/rewardsOverTimeDecomp.js index 8736acf..b874fd5 100644 --- a/mixtape/core/static/mixtape/core/insights/rewardsOverTimeDecomp.js +++ b/mixtape/core/static/mixtape/core/insights/rewardsOverTimeDecomp.js @@ -35,7 +35,7 @@ document.addEventListener('alpine:init', () => { this.$watch('$store.settings.showPlotLegends', () => { this.$nextTick(() => { Plotly.relayout( - this.$refs.rewardsOverTime, + this.$el, { autosize: true, showlegend: this.$store.settings.showPlotLegends @@ -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) ); @@ -70,11 +70,11 @@ document.addEventListener('alpine:init', () => { // Add annotations for current step values this.layout.annotations = this.createAnnotations(); - this.plot = Plotly.newPlot(this.$refs.rewardsOverTime, this.data, this.layout, {displayModeBar: false}); + this.plot = Plotly.newPlot(this.$el, this.data, this.layout, {displayModeBar: false}); }, resizePlot: _.debounce(function() { - if (!this.$refs.rewardsOverTime.querySelector('.plotly')) return; - Plotly.Plots.resize(this.$refs.rewardsOverTime); + if (!this.$el.querySelector('.plotly')) return; + Plotly.Plots.resize(this.$el); }, 200, {leading: true}), createShapes(allYValues) { const maxY = Math.max(...allYValues); @@ -82,8 +82,8 @@ document.addEventListener('alpine:init', () => { return [{ type: 'line', - x0: this.currentStep, - x1: this.currentStep, + x0: this.$store.insights.currentStep, + x1: this.$store.insights.currentStep, y0: minY, y1: maxY, line: { @@ -94,15 +94,15 @@ 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]) => { - const currentValue = rewardData[this.currentStep] || rewardData[rewardData.length - 1]; + const currentValue = rewardData[this.$store.insights.currentStep] || rewardData[rewardData.length - 1]; return { - x: this.currentStep, + x: this.$store.insights.currentStep, y: currentValue, text: currentValue.toFixed(2), showarrow: true, @@ -124,13 +124,13 @@ 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); }); }); - Plotly.relayout(this.$refs.rewardsOverTime, { + Plotly.relayout(this.$el, { shapes: this.createShapes(allYValues), annotations: this.createAnnotations() }); diff --git a/mixtape/core/static/mixtape/core/js/alpine_insights_store.js b/mixtape/core/static/mixtape/core/js/alpine_insights_store.js deleted file mode 100644 index c81abf6..0000000 --- a/mixtape/core/static/mixtape/core/js/alpine_insights_store.js +++ /dev/null @@ -1,10 +0,0 @@ -document.addEventListener('alpine:init', () => { - Alpine.store('insights', - { - // User-configurable flags - episodeSummaries: [], - // Runtime values - currentStep: 0, - } - ); -}); diff --git a/mixtape/core/static/mixtape/core/css/theme.css b/mixtape/core/static/mixtape/core/theme.css similarity index 100% rename from mixtape/core/static/mixtape/core/css/theme.css rename to mixtape/core/static/mixtape/core/theme.css diff --git a/mixtape/core/static/mixtape/core/js/alpine_theme_store.js b/mixtape/core/static/mixtape/core/themeStore.js similarity index 100% rename from mixtape/core/static/mixtape/core/js/alpine_theme_store.js rename to mixtape/core/static/mixtape/core/themeStore.js diff --git a/mixtape/core/templates/core/insights/action_v_frequency.html b/mixtape/core/templates/core/insights/action_v_frequency.html deleted file mode 100644 index d3b8d1a..0000000 --- a/mixtape/core/templates/core/insights/action_v_frequency.html +++ /dev/null @@ -1,62 +0,0 @@ -
-
-
diff --git a/mixtape/core/templates/core/insights/action_v_reward_frequency.html b/mixtape/core/templates/core/insights/action_v_reward_frequency.html deleted file mode 100644 index 0a94be1..0000000 --- a/mixtape/core/templates/core/insights/action_v_reward_frequency.html +++ /dev/null @@ -1,61 +0,0 @@ -
-
-
diff --git a/mixtape/core/templates/core/insights/clustering.html b/mixtape/core/templates/core/insights/clustering.html deleted file mode 100644 index 8bd14da..0000000 --- a/mixtape/core/templates/core/insights/clustering.html +++ /dev/null @@ -1,212 +0,0 @@ -
-
-
-
diff --git a/mixtape/core/templates/core/insights/clustering_timeline.html b/mixtape/core/templates/core/insights/clustering_timeline.html deleted file mode 100644 index fce7143..0000000 --- a/mixtape/core/templates/core/insights/clustering_timeline.html +++ /dev/null @@ -1,156 +0,0 @@ -
-
-
-
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 @@ -