ui: portfolio actions row + AI analysis regenerate
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 <noreply@anthropic.com>
This commit is contained in:
parent
652995feea
commit
736d161990
2 changed files with 80 additions and 12 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -372,13 +372,24 @@
|
|||
'</tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>' +
|
||||
// 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.
|
||||
'<div class="pf-actions">' +
|
||||
'<button id="pf-analyze" type="button">Generate AI analysis</button>' +
|
||||
(pie.analysis && pie.analysis.content
|
||||
? ''
|
||||
: '<button id="pf-analyze" type="button">Generate AI analysis</button>') +
|
||||
'<button id="pf-forget" type="button" class="pf-secondary">Forget this pie</button>' +
|
||||
'</div>' +
|
||||
'<div id="pf-analysis" class="pf-analysis" hidden></div>';
|
||||
|
||||
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 @@
|
|||
'<span class="pf-analysis__head-left">' +
|
||||
'AI analysis' +
|
||||
'</span>' +
|
||||
'<span class="pf-analysis__head-right">' +
|
||||
'<span class="pf-as-of">' +
|
||||
esc((analysis.generated_at || '').slice(0, 19).replace('T', ' ')) +
|
||||
' UTC</span>' +
|
||||
(onRegenerate
|
||||
? '<button id="pf-regen" type="button" class="pf-regen"' +
|
||||
' title="Run the analysis again on the current portfolio">' +
|
||||
'Regenerate</button>'
|
||||
: '') +
|
||||
'</span>' +
|
||||
'</summary>' +
|
||||
'<pre class="pf-analysis__body">' + esc(analysis.content) + '</pre>' +
|
||||
'</details>';
|
||||
if (onRegenerate) {
|
||||
const regen = document.getElementById('pf-regen');
|
||||
if (regen) {
|
||||
regen.addEventListener('click', (e) => {
|
||||
// The button lives inside <summary>; clicking it would
|
||||
// normally toggle the <details> 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 = '<div class="empty">generating…</div>';
|
||||
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 = '<div class="pf-warn">' + esc(e.message) + '</div>';
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue