106 lines
4.0 KiB
JavaScript
106 lines
4.0 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Content formatter (Session 29).
|
|
*
|
|
* Transforms structured content objects (from contentTemplateService) into
|
|
* platform-ready PLAIN TEXT — suitable for X threads / Telegram. Image
|
|
* formatting is a separate design-layer concern. Pure + defensive: a
|
|
* missing field renders as a sensible blank, never "undefined".
|
|
*/
|
|
|
|
function fmtOdds(o) {
|
|
const n = Number(o);
|
|
if (!Number.isFinite(n)) return String(o ?? '');
|
|
return n > 0 ? `+${n}` : `${n}`;
|
|
}
|
|
|
|
function fmtTime(iso) {
|
|
if (!iso) return '';
|
|
try {
|
|
return new Date(iso).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZoneName: 'short' });
|
|
} catch {
|
|
return String(iso);
|
|
}
|
|
}
|
|
|
|
function formatPickPost(p) {
|
|
const lines = [`${p.player} · ${p.stat} ${p.side || ''} ${p.line ?? ''}`.replace(/\s+/g, ' ').trim()];
|
|
lines.push(`Grade: ${p.grade}${p.confidence ? ` · ${p.confidence}% confidence` : ''}`);
|
|
if (p.edge) lines.push(`Edge: ${p.edge > 0 ? '+' : ''}${p.edge}%`);
|
|
if (p.analysis) lines.push(p.analysis);
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function formatGameHighlight(p) {
|
|
const parts = [`${p.game}${p.time ? ` · ${fmtTime(p.time)}` : ''}`];
|
|
if (p.bestAwayML) parts.push(`Best away ML: ${fmtOdds(p.bestAwayML.odds)} (${p.bestAwayML.book})`);
|
|
if (p.bestHomeML) parts.push(`Best home ML: ${fmtOdds(p.bestHomeML.odds)} (${p.bestHomeML.book})`);
|
|
if (p.total != null) parts.push(`O/U ${p.total}`);
|
|
if (p.bookCount) parts.push(`${p.bookCount} books`);
|
|
return parts.join('\n');
|
|
}
|
|
|
|
function formatMovers(p) {
|
|
const head = '📈 Biggest line moves:';
|
|
const rows = (p.movers || []).map((m) => {
|
|
const d = m.delta > 0 ? `+${m.delta}` : `${m.delta}`;
|
|
return `${m.player} ${String(m.stat || '').replace(/_/g, ' ')}: ${m.opening ?? '?'} → ${m.current ?? '?'} (${d})${m.sharpSignal ? ' ⚡' : ''}`;
|
|
});
|
|
return [head, ...rows].join('\n');
|
|
}
|
|
|
|
function formatSchedule(p) {
|
|
return (p.games || [])
|
|
.map((g) => `${g.away || '?'} @ ${g.home || '?'}${g.time ? ` · ${fmtTime(g.time)}` : ''}`)
|
|
.join('\n');
|
|
}
|
|
|
|
function formatPost(post) {
|
|
switch (post.role) {
|
|
case 'hook': return post.text || '';
|
|
case 'cta': return post.text || '';
|
|
case 'pick': return formatPickPost(post);
|
|
case 'game_highlight': return formatGameHighlight(post);
|
|
case 'movers': return formatMovers(post);
|
|
case 'schedule': return formatSchedule(post);
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
/** Returns an array of post-ready strings (one per thread post). */
|
|
function formatSlateThread(thread) {
|
|
if (!thread || !Array.isArray(thread.posts)) return [];
|
|
return thread.posts.map(formatPost);
|
|
}
|
|
|
|
/** Single-block POTD text. */
|
|
function formatPOTD(potd) {
|
|
if (!potd || potd.available === false) return 'No standout pick today — check the full slate at vyndr.app.';
|
|
if (potd.dataLevel === 'lines') {
|
|
return `🎯 GAME OF THE DAY\n${potd.game}${potd.time ? ` · ${fmtTime(potd.time)}` : ''}\n${potd.total != null ? `O/U ${potd.total}` : ''}${potd.bestHomeML ? `\nBest home ML ${fmtOdds(potd.bestHomeML.odds)} (${potd.bestHomeML.book})` : ''}`.trim();
|
|
}
|
|
return `🎯 PROP OF THE DAY\n${formatPickPost(potd)}`;
|
|
}
|
|
|
|
/** Recap text block. */
|
|
function formatRecap(recap) {
|
|
if (!recap || recap.available === false) return 'No graded results in yet today.';
|
|
const { record, winRate } = recap;
|
|
const lines = [`📊 VYNDR RECAP — ${recap.date}`];
|
|
lines.push(`Record: ${record.wins}-${record.losses}${record.pushes ? `-${record.pushes}` : ''}${winRate != null ? ` (${Math.round(winRate * 100)}%)` : ''}`);
|
|
if (recap.topHits && recap.topHits.length) {
|
|
lines.push('Top hits:');
|
|
for (const h of recap.topHits) lines.push(`✅ ${h.player} ${h.stat} ${h.side || ''} ${h.line ?? ''}`.replace(/\s+/g, ' ').trim());
|
|
}
|
|
if (recap.metrics && recap.metrics.brierScore != null) lines.push(`Brier: ${recap.metrics.brierScore}`);
|
|
return lines.join('\n');
|
|
}
|
|
|
|
module.exports = {
|
|
formatSlateThread,
|
|
formatPOTD,
|
|
formatRecap,
|
|
__internals: { formatPost, formatPickPost, formatGameHighlight, formatMovers, formatSchedule },
|
|
};
|