From 736d161990ac7ce48248039ef8d992b80d3bf57b Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Fri, 29 May 2026 15:04:08 +0200 Subject: [PATCH] ui: portfolio actions row + AI analysis regenerate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small UX changes to the portfolio panel: 1. "Forget this pie" is destructive enough to belong in edit-mode only. The button now hides by default and only surfaces when the #portfolio-panel.pf-editing class is on the panel (same surface that already shows per-row × and the add-position form). The element stays in the DOM so the existing click handler keeps working without re-mount. 2. "Generate AI analysis" disappears once an analysis exists. In its place a small "Regenerate" button is rendered inside the collapsible analysis box — in the summary header, right-aligned next to the timestamp. The button stops the summary's default toggle action so a click regenerates without collapsing the panel. runAnalysis() now tolerates either pf-analyze or pf-regen as the trigger, and showAnalysis() takes an optional onRegenerate callback so callers can wire the button to the current pie/enriched closure context. Re-hydration after the 60s portfolio refresh passes the same callback so the button survives a refresh cycle. Co-Authored-By: Claude Opus 4.7 --- app/static/css/portfolio.css | 25 ++++++++++++++ app/static/js/portfolio.js | 67 +++++++++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/app/static/css/portfolio.css b/app/static/css/portfolio.css index cdf0417..83b13d0 100644 --- a/app/static/css/portfolio.css +++ b/app/static/css/portfolio.css @@ -86,6 +86,13 @@ .pf-actions button:disabled { opacity: 0.5; cursor: not-allowed; } .pf-secondary { color: var(--muted); } .pf-secondary:hover { color: var(--negative); border-color: var(--negative); } +/* "Forget this pie" is destructive — only show it while the user is + in edit mode (the same mode that surfaces the per-row delete × and + the add-position form). Outside of edit mode the row stays in the + DOM so existing handlers and any future surface that wants to + toggle it can do so without re-rendering. */ +#pf-forget { display: none; } +#portfolio-panel.pf-editing #pf-forget { display: inline-block; } .pf-analysis { margin-top: 14px; @@ -107,6 +114,24 @@ list-style: none; /* hide native marker in Firefox */ } .pf-analysis__head::-webkit-details-marker { display: none; } +.pf-analysis__head-right { + display: inline-flex; + align-items: center; + gap: 12px; +} +.pf-regen { + background: transparent; + border: 1px solid var(--border); + color: var(--muted); + padding: 3px 9px; + font: inherit; + font-size: 10.5px; + letter-spacing: inherit; + cursor: pointer; + text-transform: inherit; +} +.pf-regen:hover { color: var(--accent); border-color: var(--accent); } +.pf-regen:disabled { opacity: 0.5; cursor: not-allowed; } .pf-analysis__head-left::before { content: "▸ "; display: inline-block; diff --git a/app/static/js/portfolio.js b/app/static/js/portfolio.js index 0f3ecb4..767f078 100644 --- a/app/static/js/portfolio.js +++ b/app/static/js/portfolio.js @@ -372,13 +372,24 @@ '' + '' + rows + '' + '' + + // The "Generate" button only renders when there's no cached + // analysis yet. Once one exists, regeneration moves inside the + // collapsible analysis box (see showAnalysis below). The "Forget + // this pie" button is destructive enough that it lives in + // edit-mode only — CSS in portfolio.css hides it when the + // portfolio panel isn't carrying the .pf-editing class. '
' + - '' + + (pie.analysis && pie.analysis.content + ? '' + : '') + '' + '
' + ''; - document.getElementById('pf-analyze').addEventListener('click', () => runAnalysis(pie, enriched)); + const analyzeBtn = document.getElementById('pf-analyze'); + if (analyzeBtn) { + analyzeBtn.addEventListener('click', () => runAnalysis(pie, enriched)); + } document.getElementById('pf-forget').addEventListener('click', () => { if (confirm('Remove the saved pie from this browser? The server holds nothing — this is local.')) { clearPie(); @@ -390,13 +401,16 @@ // wipe it. Rendered expanded so the user keeps seeing the body they // just generated — collapsing it under their cursor every minute // reads as "the analysis disappeared". They can still click the - // header to collapse manually within a single refresh window. + // header to collapse manually within a single refresh window. The + // regenerate callback closes over the current pie/enriched so a + // click rebuilds the analysis with the same context that drove + // the initial render. if (pie.analysis && pie.analysis.content) { - showAnalysis(pie.analysis, { open: true }); + showAnalysis(pie.analysis, { open: true }, () => runAnalysis(pie, enriched)); } } - function showAnalysis(analysis, opts) { + function showAnalysis(analysis, opts, onRegenerate) { const out = document.getElementById('pf-analysis'); if (!out) return; const openAttr = (opts && opts.open) ? ' open' : ''; @@ -407,20 +421,43 @@ '' + 'AI analysis' + '' + - '' + - esc((analysis.generated_at || '').slice(0, 19).replace('T', ' ')) + - ' UTC' + + '' + + '' + + esc((analysis.generated_at || '').slice(0, 19).replace('T', ' ')) + + ' UTC' + + (onRegenerate + ? '' + : '') + + '' + '' + '
' + esc(analysis.content) + '
' + ''; + if (onRegenerate) { + const regen = document.getElementById('pf-regen'); + if (regen) { + regen.addEventListener('click', (e) => { + // The button lives inside ; clicking it would + // normally toggle the
open/closed. Suppress the + // default toggle and the bubble so only our regen runs. + e.preventDefault(); + e.stopPropagation(); + onRegenerate(); + }); + } + } } async function runAnalysis(pie, enriched) { const out = document.getElementById('pf-analysis'); - const btn = document.getElementById('pf-analyze'); + // First-run click is on pf-analyze; the regenerate path is pf-regen + // inside the details summary. Either may be the live trigger. + const btn = document.getElementById('pf-analyze') || + document.getElementById('pf-regen'); out.hidden = false; out.innerHTML = '
generating…
'; - btn.disabled = true; + if (btn) btn.disabled = true; // Build the prices payload from the universe cache so the server // doesn't have to re-fetch. @@ -458,11 +495,17 @@ } // Persist before rendering so auto-refresh can re-hydrate. saveAnalysis(data); - showAnalysis(data, { open: true }); + // Pass the regenerate callback so the in-details "Regenerate" + // button shows up on the freshly-rendered analysis too. + showAnalysis(data, { open: true }, () => runAnalysis(pie, enriched)); } catch (e) { out.innerHTML = '
' + esc(e.message) + '
'; } finally { - btn.disabled = false; + // The original button may have been replaced by showAnalysis → + // re-fetch its handle (or null if neither id is on the page now). + const liveBtn = document.getElementById('pf-analyze') || + document.getElementById('pf-regen'); + if (liveBtn) liveBtn.disabled = false; } }