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-actions button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
.pf-secondary { color: var(--muted); }
|
.pf-secondary { color: var(--muted); }
|
||||||
.pf-secondary:hover { color: var(--negative); border-color: var(--negative); }
|
.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 {
|
.pf-analysis {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
|
|
@ -107,6 +114,24 @@
|
||||||
list-style: none; /* hide native marker in Firefox */
|
list-style: none; /* hide native marker in Firefox */
|
||||||
}
|
}
|
||||||
.pf-analysis__head::-webkit-details-marker { display: none; }
|
.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 {
|
.pf-analysis__head-left::before {
|
||||||
content: "▸ ";
|
content: "▸ ";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -372,13 +372,24 @@
|
||||||
'</tr></thead>' +
|
'</tr></thead>' +
|
||||||
'<tbody>' + rows + '</tbody>' +
|
'<tbody>' + rows + '</tbody>' +
|
||||||
'</table>' +
|
'</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">' +
|
'<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>' +
|
'<button id="pf-forget" type="button" class="pf-secondary">Forget this pie</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div id="pf-analysis" class="pf-analysis" hidden></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', () => {
|
document.getElementById('pf-forget').addEventListener('click', () => {
|
||||||
if (confirm('Remove the saved pie from this browser? The server holds nothing — this is local.')) {
|
if (confirm('Remove the saved pie from this browser? The server holds nothing — this is local.')) {
|
||||||
clearPie();
|
clearPie();
|
||||||
|
|
@ -390,13 +401,16 @@
|
||||||
// wipe it. Rendered expanded so the user keeps seeing the body they
|
// wipe it. Rendered expanded so the user keeps seeing the body they
|
||||||
// just generated — collapsing it under their cursor every minute
|
// just generated — collapsing it under their cursor every minute
|
||||||
// reads as "the analysis disappeared". They can still click the
|
// 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) {
|
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');
|
const out = document.getElementById('pf-analysis');
|
||||||
if (!out) return;
|
if (!out) return;
|
||||||
const openAttr = (opts && opts.open) ? ' open' : '';
|
const openAttr = (opts && opts.open) ? ' open' : '';
|
||||||
|
|
@ -407,20 +421,43 @@
|
||||||
'<span class="pf-analysis__head-left">' +
|
'<span class="pf-analysis__head-left">' +
|
||||||
'AI analysis' +
|
'AI analysis' +
|
||||||
'</span>' +
|
'</span>' +
|
||||||
'<span class="pf-as-of">' +
|
'<span class="pf-analysis__head-right">' +
|
||||||
esc((analysis.generated_at || '').slice(0, 19).replace('T', ' ')) +
|
'<span class="pf-as-of">' +
|
||||||
' UTC</span>' +
|
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>' +
|
'</summary>' +
|
||||||
'<pre class="pf-analysis__body">' + esc(analysis.content) + '</pre>' +
|
'<pre class="pf-analysis__body">' + esc(analysis.content) + '</pre>' +
|
||||||
'</details>';
|
'</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) {
|
async function runAnalysis(pie, enriched) {
|
||||||
const out = document.getElementById('pf-analysis');
|
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.hidden = false;
|
||||||
out.innerHTML = '<div class="empty">generating…</div>';
|
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
|
// Build the prices payload from the universe cache so the server
|
||||||
// doesn't have to re-fetch.
|
// doesn't have to re-fetch.
|
||||||
|
|
@ -458,11 +495,17 @@
|
||||||
}
|
}
|
||||||
// Persist before rendering so auto-refresh can re-hydrate.
|
// Persist before rendering so auto-refresh can re-hydrate.
|
||||||
saveAnalysis(data);
|
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) {
|
} catch (e) {
|
||||||
out.innerHTML = '<div class="pf-warn">' + esc(e.message) + '</div>';
|
out.innerHTML = '<div class="pf-warn">' + esc(e.message) + '</div>';
|
||||||
} finally {
|
} 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