feat: surface automation option details

This commit is contained in:
TopherMayor
2026-04-30 11:07:26 -07:00
parent 899c9cb30b
commit b7a4386e00
4 changed files with 257 additions and 4 deletions

View File

@@ -589,6 +589,67 @@
margin-bottom: 8px;
line-height: 1.4;
}
.option-facts {
display: flex;
flex-direction: column;
gap: 8px;
margin: 8px 0 2px;
}
.option-facts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 6px;
}
.option-fact {
padding: 8px 10px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.option-fact-label {
display: block;
font-size: 0.58rem;
text-transform: uppercase;
letter-spacing: 0.9px;
color: var(--text-muted);
margin-bottom: 3px;
}
.option-fact-value {
font-size: 0.73rem;
color: #fff;
line-height: 1.4;
}
.option-fact-note {
font-size: 0.68rem;
color: var(--text-muted);
line-height: 1.4;
}
.option-detail-section {
margin-top: 2px;
padding: 8px 10px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.025);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.option-detail-title {
font-size: 0.58rem;
text-transform: uppercase;
letter-spacing: 0.9px;
color: var(--text-muted);
margin-bottom: 5px;
}
.option-detail-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.option-detail-chip {
background: var(--surface2);
border-radius: 999px;
padding: 3px 8px;
font-size: 0.65rem;
color: var(--text-muted);
}
.option-link {
display: inline-block;
font-size: 0.7rem;
@@ -1492,6 +1553,135 @@
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function normalizeTextList(value) {
const items = Array.isArray(value) ? value : value == null ? [] : [value];
return [...new Set(items.flatMap(item => {
if (Array.isArray(item)) return item;
if (item && typeof item === 'object') {
return [
item.label,
item.name,
item.text,
item.title,
item.value,
item.summary,
item.description,
].filter(Boolean);
}
return [item];
})
.map(item => String(item).trim())
.filter(Boolean))];
}
function renderTextChips(items) {
const chips = normalizeTextList(items);
if (!chips.length) return '';
return `
<div class="option-detail-list">
${chips.map(item => `<span class="option-detail-chip">${escapeHtml(item)}</span>`).join('')}
</div>
`;
}
function renderOptionFacts(opt) {
const insights = opt.automationInsights || {};
const currentPrice = typeof opt.currentPrice === 'number' ? formatCurrency(opt.currentPrice, insights.currency || 'USD') : '';
const priceLabel = insights.displayPrice || currentPrice || 'Not yet tracked';
const sourceLabel = insights.source || 'Automation feed';
const statusLabel = insights.availability || insights.decisionNote || 'Matched from live search';
const overviewItems = normalizeTextList(opt.details);
const autoHighlights = normalizeTextList(insights.highlights);
const features = normalizeTextList(insights.features);
const amenities = normalizeTextList(insights.amenities);
const inclusions = normalizeTextList(insights.inclusions);
const limitations = normalizeTextList(insights.limitations);
const sections = [];
sections.push(`
<div class="option-facts-grid">
<div class="option-fact">
<span class="option-fact-label">Current price</span>
<div class="option-fact-value">${escapeHtml(priceLabel)}</div>
</div>
<div class="option-fact">
<span class="option-fact-label">Source</span>
<div class="option-fact-value">${escapeHtml(sourceLabel)}</div>
</div>
<div class="option-fact">
<span class="option-fact-label">Status</span>
<div class="option-fact-value">${escapeHtml(statusLabel)}</div>
</div>
</div>
`);
if (overviewItems.length) {
sections.push(`
<div class="option-detail-section">
<div class="option-detail-title">Decision summary</div>
${renderTextChips([...overviewItems, ...autoHighlights])}
</div>
`);
} else if (autoHighlights.length) {
sections.push(`
<div class="option-detail-section">
<div class="option-detail-title">Decision summary</div>
${renderTextChips(autoHighlights)}
</div>
`);
}
if (features.length) {
sections.push(`
<div class="option-detail-section">
<div class="option-detail-title">Features</div>
${renderTextChips(features)}
</div>
`);
}
if (amenities.length) {
sections.push(`
<div class="option-detail-section">
<div class="option-detail-title">Amenities</div>
${renderTextChips(amenities)}
</div>
`);
}
if (inclusions.length) {
sections.push(`
<div class="option-detail-section">
<div class="option-detail-title">Inclusions</div>
${renderTextChips(inclusions)}
</div>
`);
}
if (limitations.length) {
sections.push(`
<div class="option-detail-section">
<div class="option-detail-title">Tradeoffs</div>
${renderTextChips(limitations)}
</div>
`);
}
return sections.length
? `<div class="option-facts">${sections.join('')}</div>`
: '';
}
function renderPriceTrend(opt) {
const series = Array.isArray(opt.priceHistory)
? opt.priceHistory.filter(point => typeof point.price === 'number' && !Number.isNaN(point.price))
@@ -1753,9 +1943,7 @@
</div>
${opt.desc ? `<div class="option-desc">${opt.desc}</div>` : ''}
${linkPills}
${opt.details && opt.details.length ? `
<div class="itin-details">${opt.details.map(d => `<span class="itin-tag">${d}</span>`).join('')}</div>
` : ''}
${renderOptionFacts(opt)}
${renderPriceTrend(opt)}
<div class="vote-bar-bg">
<div class="vote-bar-fill ${catClass}" style="width:${votePct}%"></div>