feat: Nous Portal news summarizer, ticker flags, map pins #8
1 changed files with 47 additions and 21 deletions
|
|
@ -539,11 +539,14 @@
|
||||||
.tick-item .dom { font-family: 'Share Tech Mono', monospace; font-size: 0.64rem; color: var(--cyan); background: rgba(53,224,255,0.08); border: 1px solid rgba(53,224,255,0.22); border-radius: 3px; padding: 0.05rem 0.4rem; }
|
.tick-item .dom { font-family: 'Share Tech Mono', monospace; font-size: 0.64rem; color: var(--cyan); background: rgba(53,224,255,0.08); border: 1px solid rgba(53,224,255,0.22); border-radius: 3px; padding: 0.05rem 0.4rem; }
|
||||||
.tick-item .tt { font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--muted); }
|
.tick-item .tt { font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--muted); }
|
||||||
.tick-item .sep { color: var(--line-hi); }
|
.tick-item .sep { color: var(--line-hi); }
|
||||||
|
button.tick-item { cursor: pointer; background: transparent; border: 0; font: inherit; color: inherit; }
|
||||||
.tick-item.brief {
|
.tick-item.brief {
|
||||||
cursor: pointer; background: linear-gradient(90deg, rgba(255,46,151,0.12), transparent 70%);
|
cursor: pointer; background: linear-gradient(90deg, rgba(255,46,151,0.12), transparent 70%);
|
||||||
border-left: 2px solid var(--magenta); font-weight: 600;
|
border-left: 2px solid var(--magenta); font-weight: 600;
|
||||||
}
|
}
|
||||||
.tick-item.brief .b-tag { font-family: 'Share Tech Mono', monospace; font-size: 0.6rem; color: #1c0311; background: var(--magenta); border-radius: 2px; padding: 0.08rem 0.4rem; letter-spacing: 0.1em; }
|
.tick-item .b-tag { font-family: 'Share Tech Mono', monospace; font-size: 0.6rem; color: #1c0311; background: var(--magenta); border-radius: 2px; padding: 0.08rem 0.4rem; letter-spacing: 0.1em; }
|
||||||
|
.tick-item .b-tag.critical { background: var(--red); }
|
||||||
|
.tick-item .b-tag.high { background: var(--amber); }
|
||||||
.tick-item.brief:hover { color: var(--magenta); }
|
.tick-item.brief:hover { color: var(--magenta); }
|
||||||
.tick-item.standby { color: var(--muted); opacity: 0.75; }
|
.tick-item.standby { color: var(--muted); opacity: 0.75; }
|
||||||
.tick-item.standby .price { color: var(--muted); }
|
.tick-item.standby .price { color: var(--muted); }
|
||||||
|
|
@ -827,7 +830,7 @@
|
||||||
<div class="ns-kicker">Executive Summary <span class="ns-badge" id="ns-badge">WAITING</span></div>
|
<div class="ns-kicker">Executive Summary <span class="ns-badge" id="ns-badge">WAITING</span></div>
|
||||||
<div class="ns-time" id="ns-time">—</div>
|
<div class="ns-time" id="ns-time">—</div>
|
||||||
<div class="ns-body" id="ns-body">
|
<div class="ns-body" id="ns-body">
|
||||||
<div class="ns-empty">No executive summary yet — the Gemini summarizer is idle until <code>GEMINI_API_KEY</code> is set on the Pi.</div>
|
<div class="ns-empty">No executive summary yet — the Nous summarizer is idle until <code>NOUS_API_KEY</code> is set on the Pi and a model is chosen in Settings.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1323,13 +1326,16 @@ async function loadNews(force) {
|
||||||
if (upd) upd.textContent = 'updating…';
|
if (upd) upd.textContent = 'updating…';
|
||||||
let articles = [];
|
let articles = [];
|
||||||
let summaries = [];
|
let summaries = [];
|
||||||
|
let tickerItems = [];
|
||||||
try {
|
try {
|
||||||
const [rArticles, rSumm] = await Promise.all([
|
const [rArticles, rSumm, rTick] = await Promise.all([
|
||||||
fetch(`${API}/api/news?limit=100`),
|
fetch(`${API}/api/news?limit=100`),
|
||||||
fetch(`${API}/api/news/summaries?limit=1`)
|
fetch(`${API}/api/news/summaries?limit=1`),
|
||||||
|
fetch(`${API}/api/news/ticker?limit=20`),
|
||||||
]);
|
]);
|
||||||
if (rArticles.ok) articles = await rArticles.json();
|
if (rArticles.ok) articles = await rArticles.json();
|
||||||
if (rSumm.ok) summaries = await rSumm.json();
|
if (rSumm.ok) summaries = await rSumm.json();
|
||||||
|
if (rTick.ok) tickerItems = await rTick.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const list = document.getElementById('news-list');
|
const list = document.getElementById('news-list');
|
||||||
const body = document.getElementById('ns-body');
|
const body = document.getElementById('ns-body');
|
||||||
|
|
@ -1341,7 +1347,7 @@ async function loadNews(force) {
|
||||||
}
|
}
|
||||||
renderNewsSummary(summaries);
|
renderNewsSummary(summaries);
|
||||||
renderNewsList(articles);
|
renderNewsList(articles);
|
||||||
renderNewsTicker(articles, summaries);
|
renderNewsTicker(articles, summaries, tickerItems);
|
||||||
if (upd) upd.textContent = 'updated ' + new Date().toLocaleTimeString();
|
if (upd) upd.textContent = 'updated ' + new Date().toLocaleTimeString();
|
||||||
if (!newsInterval) {
|
if (!newsInterval) {
|
||||||
newsInterval = setInterval(() => loadNews(false), NEWS_REFRESH_MS);
|
newsInterval = setInterval(() => loadNews(false), NEWS_REFRESH_MS);
|
||||||
|
|
@ -1353,7 +1359,7 @@ function renderNewsSummary(summaries) {
|
||||||
const badgeEl = document.getElementById('ns-badge');
|
const badgeEl = document.getElementById('ns-badge');
|
||||||
if (!bodyEl) return;
|
if (!bodyEl) return;
|
||||||
if (!summaries || !summaries.length) {
|
if (!summaries || !summaries.length) {
|
||||||
bodyEl.innerHTML = '<div class="ns-empty">No executive summary yet — the Gemini summarizer is idle until <code>GEMINI_API_KEY</code> is set on the Pi.</div>';
|
bodyEl.innerHTML = '<div class="ns-empty">No executive summary yet — the Nous summarizer is idle until <code>NOUS_API_KEY</code> is set on the Pi and a model is chosen in Settings.</div>';
|
||||||
timeEl.textContent = '—';
|
timeEl.textContent = '—';
|
||||||
badgeEl.textContent = 'WAITING';
|
badgeEl.textContent = 'WAITING';
|
||||||
return;
|
return;
|
||||||
|
|
@ -1381,25 +1387,45 @@ function renderNewsList(articles) {
|
||||||
'</a>';
|
'</a>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
function renderNewsTicker(articles, summaries) {
|
function renderNewsTicker(articles, summaries, tickerItems) {
|
||||||
const track = document.getElementById('nt-track');
|
const track = document.getElementById('nt-track');
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
let items = '';
|
let items = '';
|
||||||
// LLM executive summary flash first (click → News view)
|
const ticks = Array.isArray(tickerItems) ? tickerItems : [];
|
||||||
const summ = summaries && summaries.length ? summaries[0] : null;
|
if (ticks.length) {
|
||||||
if (summ && summ.summary_text) {
|
ticks.forEach(t => {
|
||||||
const brief = summ.summary_text.replace(/\s+/g, ' ').trim();
|
const imp = String(t.importance || '').toLowerCase();
|
||||||
items += `<button class="tick-item brief" onclick="showView('news')" title="Open full brief">` +
|
const tag = imp === 'critical' ? 'CRITICAL' : 'HIGH';
|
||||||
`<span class="b-tag">LLM BRIEF</span><span>${newsEsc(brief.slice(0, 200))}${brief.length > 200 ? '…' : ''}</span>` +
|
const loc = (t.location_name || '').trim();
|
||||||
`<span class="tt">${newsTimeAgo(summ.batch_timestamp)}</span><span class="sep">▸</span></button>`;
|
const inner =
|
||||||
|
`<span class="b-tag ${imp === 'critical' ? 'critical' : 'high'}">${tag}</span>` +
|
||||||
|
`<span>${newsEsc((t.headline || '').trim())}</span>` +
|
||||||
|
(loc ? `<span class="dom">${newsEsc(loc)}</span>` : '') +
|
||||||
|
`<span class="tt">${newsTimeAgo(t.created_at)}</span><span class="sep">▸</span>`;
|
||||||
|
const url = (t.url || '').trim();
|
||||||
|
if (url) {
|
||||||
|
items += `<a class="tick-item" href="${newsEsc(url)}" target="_blank" rel="noopener">${inner}</a>`;
|
||||||
|
} else {
|
||||||
|
items += `<button class="tick-item" onclick="showView('news')" title="Open news brief">${inner}</button>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Quiet hour: brief chip + article titles so the dock is never blank.
|
||||||
|
const summ = summaries && summaries.length ? summaries[0] : null;
|
||||||
|
const briefSrc = summ && (summ.summary_en || summ.summary_text);
|
||||||
|
if (briefSrc) {
|
||||||
|
const brief = String(briefSrc).replace(/\s+/g, ' ').trim();
|
||||||
|
items += `<button class="tick-item brief" onclick="showView('news')" title="Open full brief">` +
|
||||||
|
`<span class="b-tag">LLM BRIEF</span><span>${newsEsc(brief.slice(0, 120))}${brief.length > 120 ? '…' : ''}</span>` +
|
||||||
|
`<span class="tt">${newsTimeAgo(summ.batch_timestamp)}</span><span class="sep">▸</span></button>`;
|
||||||
|
}
|
||||||
|
(articles || []).slice(0, 10).forEach(a => {
|
||||||
|
items += `<a class="tick-item" href="${newsEsc(a.url || '#')}" target="_blank" rel="noopener">` +
|
||||||
|
`<span class="dom">${newsEsc(a.domain || '?')}</span>` +
|
||||||
|
`<span>${newsEsc((a.title || '').trim())}</span>` +
|
||||||
|
`<span class="tt">${newsTimeAgo(a.timestamp)}</span><span class="sep">▸</span></a>`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Top headlines
|
|
||||||
(articles || []).slice(0, 30).forEach(a => {
|
|
||||||
items += `<a class="tick-item" href="${newsEsc(a.url || '#')}" target="_blank" rel="noopener">` +
|
|
||||||
`<span class="dom">${newsEsc(a.domain || '?')}</span>` +
|
|
||||||
`<span>${newsEsc((a.title || '').trim())}</span>` +
|
|
||||||
`<span class="tt">${newsTimeAgo(a.timestamp)}</span><span class="sep">▸</span></a>`;
|
|
||||||
});
|
|
||||||
if (!items) {
|
if (!items) {
|
||||||
items = `<span class="tick-item standby">No headlines yet — scraper idle</span>`;
|
items = `<span class="tick-item standby">No headlines yet — scraper idle</span>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue