Upload static/app.js with huggingface_hub
Browse files- static/app.js +115 -2
static/app.js
CHANGED
|
@@ -151,8 +151,9 @@ function fillBubble(role, phase, entry) {
|
|
| 151 |
const kpHtml = kp.length
|
| 152 |
? `<details class="kp-wrap" open><summary>${label}</summary><ul class="kp">${kp.map(k => `<li>${k.topic ? `<span class="kp-topic">${escapeHtml(k.topic)}</span> ` : ""}${escapeHtml(k.point || "")}</li>`).join("")}</ul></details>`
|
| 153 |
: "";
|
|
|
|
| 154 |
bubble.querySelector(".bubble-body").innerHTML = `
|
| 155 |
-
<p class="bubble-verdict">${num}${entry.recommendation ? " · " + escapeHtml(entry.recommendation) : ""}</p>
|
| 156 |
${entry.reasoning ? `<p class="bubble-text">${escapeHtml(entry.reasoning)}</p>` : ""}
|
| 157 |
${kpHtml}`;
|
| 158 |
}
|
|
@@ -232,6 +233,111 @@ function valuePriceScale(dm) {
|
|
| 232 |
<p class="vscale-cap">left = cheaper · right = pricier — the gap between price and intrinsic value is the signal</p></div>`;
|
| 233 |
}
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
// "How this number is built" — every source in the final blend with its effective share.
|
| 236 |
function renderBlendPanel(result) {
|
| 237 |
const host = $("#valuation"); if (!host) return;
|
|
@@ -377,7 +483,11 @@ function setTab(name) {
|
|
| 377 |
document.querySelectorAll(".tab-page").forEach(p => p.classList.toggle("active", p.id === `tab-${name}`));
|
| 378 |
if (name === "log") { const l = $("#log"); l.scrollTop = l.scrollHeight; }
|
| 379 |
}
|
| 380 |
-
$("#tabbar").addEventListener("click", e => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
/* ---------- key-numbers strip ---------- */
|
| 383 |
function renderKeyStrip(result) {
|
|
@@ -546,6 +656,8 @@ function showResult(result) {
|
|
| 546 |
renderReconcile(result);
|
| 547 |
renderValuationTab(result);
|
| 548 |
renderBlendPanel(result);
|
|
|
|
|
|
|
| 549 |
$("#tabbar .tab-btn[data-tab='valuation']")?.classList.add("ready");
|
| 550 |
const d = result.debate; if (!d) return;
|
| 551 |
fillBubble("neutral", "opening", d.roles.neutral);
|
|
@@ -664,6 +776,7 @@ async function run() {
|
|
| 664 |
$("#keystrip").hidden = true; $("#keystrip").innerHTML = "";
|
| 665 |
$("#verdict-reconcile").hidden = true; $("#verdict-reconcile").innerHTML = "";
|
| 666 |
$("#valuation").innerHTML = '<p class="placeholder">The Damodaran valuation appears when the run completes.</p>';
|
|
|
|
| 667 |
$("#tabbar .tab-btn[data-tab='valuation']")?.classList.remove("ready");
|
| 668 |
setTab("debate");
|
| 669 |
initTimeline();
|
|
|
|
| 151 |
const kpHtml = kp.length
|
| 152 |
? `<details class="kp-wrap" open><summary>${label}</summary><ul class="kp">${kp.map(k => `<li>${k.topic ? `<span class="kp-topic">${escapeHtml(k.topic)}</span> ` : ""}${escapeHtml(k.point || "")}</li>`).join("")}</ul></details>`
|
| 153 |
: "";
|
| 154 |
+
const evd = entry.evidence && entry.evidence.total ? `<span class="ev-badge" title="How many of this analyst's points cite a specific number — an evidence count, not a self-reported confidence">${entry.evidence.quantified}/${entry.evidence.total} points quantified</span>` : "";
|
| 155 |
bubble.querySelector(".bubble-body").innerHTML = `
|
| 156 |
+
<p class="bubble-verdict">${num}${entry.recommendation ? " · " + escapeHtml(entry.recommendation) : ""}${evd}</p>
|
| 157 |
${entry.reasoning ? `<p class="bubble-text">${escapeHtml(entry.reasoning)}</p>` : ""}
|
| 158 |
${kpHtml}`;
|
| 159 |
}
|
|
|
|
| 233 |
<p class="vscale-cap">left = cheaper · right = pricier — the gap between price and intrinsic value is the signal</p></div>`;
|
| 234 |
}
|
| 235 |
|
| 236 |
+
const _fmtB = v => v == null ? "—" : (Math.abs(v) >= 1000 ? `$${(v/1000).toFixed(1)}T` : `$${Number(v).toFixed(0)}B`);
|
| 237 |
+
const _fx1 = v => v == null ? "—" : Number(v).toFixed(1);
|
| 238 |
+
const _fpc = v => v == null ? "—" : `${(v * 100).toFixed(0)}%`;
|
| 239 |
+
|
| 240 |
+
// Scenario table, peer comparables, sensitivity, catalysts & view triggers (valuation tab)
|
| 241 |
+
function renderInstitutional(result) {
|
| 242 |
+
const host = $("#valuation"); if (!host) return;
|
| 243 |
+
const dm = result.damodaran || {}, d = result.debate || {}, roles = d.roles || {};
|
| 244 |
+
const price = dm.current_price;
|
| 245 |
+
let h = "";
|
| 246 |
+
// ── scenarios: bear / neutral / bull returns × PM odds → prices ──
|
| 247 |
+
if (d.p_bear != null && price) {
|
| 248 |
+
const ret = (a, b) => { const e = roles[a] || roles[b] || {}; return e.return_pct; };
|
| 249 |
+
const rows = [["Bear", ret("bear_rebuttal", "bear_initial"), d.p_bear, "down"], ["Neutral", (roles.neutral || {}).return_pct, d.p_neutral, "flat"], ["Bull", ret("bull_rebuttal", "bull_initial"), d.p_bull, "up"]];
|
| 250 |
+
const ev = rows.reduce((s, r) => s + (r[1] || 0) * (r[2] || 0), 0);
|
| 251 |
+
h += `<section class="val-sec"><h4>Scenarios</h4><div class="val-scroll"><table class="val-t"><tr><th>Scenario</th><th>Odds</th><th>12-month return</th><th>Implied price</th></tr>
|
| 252 |
+
${rows.map(r => `<tr><td class="k ${r[3]}">${r[0]}</td><td>${Math.round((r[2] || 0) * 100)}%</td><td class="${r[3]}">${r[1] != null ? (r[1] >= 0 ? "+" : "") + r[1] + "%" : "—"}</td><td>${r[1] != null ? _usd(price * (1 + r[1] / 100)) : "—"}</td></tr>`).join("")}
|
| 253 |
+
<tr><td class="k">Odds-weighted</td><td>100%</td><td><b>${ev >= 0 ? "+" : ""}${ev.toFixed(1)}%</b></td><td><b>${_usd(price * (1 + ev / 100))}</b></td></tr></table></div></section>`;
|
| 254 |
+
}
|
| 255 |
+
// ── peers ──
|
| 256 |
+
const pc = result.peer_comps;
|
| 257 |
+
if (pc && pc.peers && pc.peers.length) {
|
| 258 |
+
const sj = pc.subject || {}, pct = pc.percentiles || {};
|
| 259 |
+
const cell = (v, f) => v == null ? "—" : f(v);
|
| 260 |
+
const pctTxt = k => pct[k] != null ? `<span class="pct-cell">${pct[k]}th pct</span>` : "";
|
| 261 |
+
h += `<section class="val-sec"><h4>Relative to peers</h4>
|
| 262 |
+
<p class="val-note">${pc.peers.length} closest ${escapeHtml(pc.sector || "")} names by market cap (Sharadar panel, as of ${pc.as_of}). Percentile = share of peers the subject is above.</p>
|
| 263 |
+
<div class="val-scroll"><table class="val-t"><tr><th>Company</th><th>Mkt cap</th><th>P/E</th><th>EV/EBITDA</th><th>P/S</th><th>Op. margin</th><th>Revenue growth</th><th>FCF yield</th></tr>
|
| 264 |
+
${sj.ticker ? `<tr><td class="k"><b>${sj.ticker}</b> (subject)</td><td>${_fmtB(sj.mcap_b)}</td><td><b>${cell(sj.pe,_fx1)}</b> ${pctTxt("pe")}</td><td><b>${cell(sj.ev_ebitda,_fx1)}</b> ${pctTxt("ev_ebitda")}</td><td><b>${cell(sj.ps,_fx1)}</b> ${pctTxt("ps")}</td><td><b>${cell(sj.op_margin,_fpc)}</b> ${pctTxt("op_margin")}</td><td><b>${cell(sj.rev_growth,_fpc)}</b> ${pctTxt("rev_growth")}</td><td><b>${cell(sj.fcf_yield,_fpc)}</b> ${pctTxt("fcf_yield")}</td></tr>` : ""}
|
| 265 |
+
${pc.peers.map(p => `<tr><td class="k">${p.ticker}</td><td>${_fmtB(p.mcap_b)}</td><td>${cell(p.pe,_fx1)}</td><td>${cell(p.ev_ebitda,_fx1)}</td><td>${cell(p.ps,_fx1)}</td><td>${cell(p.op_margin,_fpc)}</td><td>${cell(p.rev_growth,_fpc)}</td><td>${cell(p.fcf_yield,_fpc)}</td></tr>`).join("")}
|
| 266 |
+
</table></div></section>`;
|
| 267 |
+
}
|
| 268 |
+
// ── sensitivity ──
|
| 269 |
+
const sens = result.sensitivity || [];
|
| 270 |
+
if (sens.length && dm.value) {
|
| 271 |
+
const cls = x => x >= 0.10 ? "up" : x <= -0.05 ? "down" : "flat";
|
| 272 |
+
h += `<section class="val-sec"><h4>How sensitive is the value?</h4>
|
| 273 |
+
<p class="val-note">The story value re-computed with one assumption moved at a time. Base case ${_usd(dm.value)}.</p>
|
| 274 |
+
<div class="val-scroll"><table class="val-t"><tr><th>Change</th><th>Value</th><th>vs base</th><th>vs price</th></tr>
|
| 275 |
+
${sens.map(r => `<tr><td class="k">${escapeHtml(r.case)}</td><td>${_usd(r.value)}</td><td class="${cls(r.vs_base)}">${_pct(r.vs_base)}</td><td class="${cls(r.implied)}">${_pct(r.implied)}</td></tr>`).join("")}
|
| 276 |
+
</table></div></section>`;
|
| 277 |
+
}
|
| 278 |
+
// ── catalysts + what would change the view ──
|
| 279 |
+
const cats = result.catalysts || [], vt = result.view_triggers || {};
|
| 280 |
+
if (cats.length || vt.more_bearish_if || vt.more_bullish_if) {
|
| 281 |
+
h += `<section class="val-sec"><h4>Catalysts & what would change the view</h4>`;
|
| 282 |
+
if (cats.length) h += `<div class="val-scroll"><table class="val-t"><tr><th>Date</th><th>Event</th><th>Type</th></tr>${cats.map(c => `<tr><td class="k">${escapeHtml(c.date || "—")}</td><td>${escapeHtml(c.event || "")}</td><td>${escapeHtml(c.kind || "")}</td></tr>`).join("")}</table></div>`;
|
| 283 |
+
if (vt.more_bearish_if) h += `<div class="trigger bear"><b>The view gets more bearish if</b> the bear's sharpest point plays out: ${escapeHtml(vt.more_bearish_if)}</div>`;
|
| 284 |
+
if (vt.more_bullish_if) h += `<div class="trigger bull"><b>The view gets more bullish if</b> the bull's sharpest point plays out: ${escapeHtml(vt.more_bullish_if)}</div>`;
|
| 285 |
+
h += `</section>`;
|
| 286 |
+
}
|
| 287 |
+
if (!h) return;
|
| 288 |
+
const foot = host.querySelector(".val-foot");
|
| 289 |
+
if (foot) foot.insertAdjacentHTML("beforebegin", h); else host.insertAdjacentHTML("beforeend", h);
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// Diagnostics tab: provenance, risk card, model reliability, scorecard, ratings, summary
|
| 293 |
+
function renderDiagnostics(result) {
|
| 294 |
+
const root = $("#diagnostics"); if (!root) return;
|
| 295 |
+
const pv = result.provenance || {}, rc = result.risk_card || {}, dg = result.diagnostics || {}, ml = dg.ml || {};
|
| 296 |
+
let h = `<div class="diag-grid">`;
|
| 297 |
+
h += `<div class="diag"><h5>Data provenance</h5>
|
| 298 |
+
<div class="row"><span>Price as of</span><b>${escapeHtml(pv.price_as_of || "—")}</b></div>
|
| 299 |
+
<div class="row"><span>Latest statements</span><b>${escapeHtml(pv.statements_latest_quarter || "—")}</b></div>
|
| 300 |
+
<div class="row"><span>Profile / TTM fields</span><b>${escapeHtml(pv.overview_latest_quarter || "—")}</b></div>
|
| 301 |
+
<div class="row"><span>Analysts in consensus</span><b>${pv.analysts_in_consensus ?? "—"}</b></div>
|
| 302 |
+
<div class="row"><span>News articles read</span><b>${pv.news_articles ?? "—"}</b></div>
|
| 303 |
+
<div class="row"><span>Cross-sectional panel</span><b>${escapeHtml(pv.panel_as_of || "—")}</b></div>
|
| 304 |
+
${pv.fx ? `<div class="row"><span>Statement currency</span><b>converted at ${Number(pv.fx).toFixed(4)} USD</b></div>` : ""}
|
| 305 |
+
${pv.quarter_mismatch ? `<div class="warn-note">The latest statements are newer than the trailing-twelve-month profile fields — some ratios lag a quarter.</div>` : ""}
|
| 306 |
+
<div class="ks-s">${escapeHtml(pv.sources || "")}</div></div>`;
|
| 307 |
+
h += `<div class="diag"><h5>Risk profile (for sizing)</h5>
|
| 308 |
+
<div class="row"><span>Beta</span><b>${rc.beta ?? "—"}</b></div>
|
| 309 |
+
<div class="row"><span>Realized volatility (1y)</span><b>${rc.vol_1y != null ? (rc.vol_1y * 100).toFixed(0) + "%" : "—"}</b></div>
|
| 310 |
+
<div class="row"><span>Max drawdown (1y)</span><b class="down">${rc.max_drawdown_1y != null ? (rc.max_drawdown_1y * 100).toFixed(0) + "%" : "—"}</b></div>
|
| 311 |
+
<div class="row"><span>From 52-week high</span><b>${rc.dist_52w_high != null ? (rc.dist_52w_high * 100).toFixed(0) + "%" : "—"}</b></div>
|
| 312 |
+
<div class="row"><span>Correlation to S&P 500</span><b>${rc.corr_spy_1y ?? "—"}</b></div>
|
| 313 |
+
<div class="row"><span>Avg daily $ volume (30d)</span><b>${rc.avg_dollar_volume_30d != null ? "$" + (rc.avg_dollar_volume_30d / 1e6).toFixed(0) + "M" : "—"}</b></div></div>`;
|
| 314 |
+
h += `<div class="diag"><h5>Price-model reliability</h5>
|
| 315 |
+
<div class="row"><span>Direction accuracy (out of sample)</span><b>${ml.dir_acc != null ? (ml.dir_acc * 100).toFixed(0) + "%" : "—"}</b></div>
|
| 316 |
+
<div class="row"><span>Typical error</span><b>${ml.mae != null ? "±" + (ml.mae * 100).toFixed(0) + "%" : "—"}</b></div>
|
| 317 |
+
<div class="row"><span>Rank correlation</span><b>${ml.rank_ic != null ? Number(ml.rank_ic).toFixed(2) : "—"}</b></div>
|
| 318 |
+
<div class="row"><span>Independent test points</span><b>${ml.n_oos_eff ?? "—"} (of ${ml.n_oos ?? "—"})</b></div>
|
| 319 |
+
<div class="row"><span>Judged reliable</span><b>${ml.reliable === true ? "yes" : ml.reliable === false ? "no" : "—"}</b></div>
|
| 320 |
+
${ml.dominance_flag ? `<div class="warn-note">${escapeHtml(String(ml.dominance_flag).replace("HIGH_DOMINANCE", "One feature dominates the model").replace("EXTREME_DOMINANCE", "A single feature dominates the model"))}</div>` : ""}</div>`;
|
| 321 |
+
const ar = dg.analyst_ratings || {};
|
| 322 |
+
const tot = ["StrongBuy", "Buy", "Hold", "Sell", "StrongSell"].reduce((s, k) => s + (Number(ar[k]) || 0), 0);
|
| 323 |
+
h += `<div class="diag"><h5>Street & market context</h5>
|
| 324 |
+
${tot ? `<div class="row"><span>Analyst ratings</span><b>${ar.StrongBuy || 0} strong buy · ${ar.Buy || 0} buy · ${ar.Hold || 0} hold · ${(Number(ar.Sell) || 0) + (Number(ar.StrongSell) || 0)} sell</b></div>` : ""}
|
| 325 |
+
${dg.quant_rank_pct != null ? `<div class="row"><span>Quant rank vs ~1,000 stocks</span><b>${dg.quant_rank_pct}th percentile</b></div>` : ""}
|
| 326 |
+
${dg.range_52w && dg.range_52w.high ? `<div class="row"><span>52-week range</span><b>${_usd(dg.range_52w.low)} – ${_usd(dg.range_52w.high)}</b></div>` : ""}
|
| 327 |
+
${dg.range_52w && dg.range_52w.sma200 ? `<div class="row"><span>50 / 200-day averages</span><b>${_usd(dg.range_52w.sma50)} / ${_usd(dg.range_52w.sma200)}</b></div>` : ""}
|
| 328 |
+
${dg.styles ? `<div class="row"><span>Style scores (z)</span><b>value ${Number(dg.styles.value).toFixed(2)} · quality ${Number(dg.styles.quality).toFixed(2)} · momentum ${Number(dg.styles.momentum).toFixed(2)}</b></div>` : ""}</div>`;
|
| 329 |
+
h += `</div>`;
|
| 330 |
+
const sc = dg.scorecard;
|
| 331 |
+
if (sc && Array.isArray(sc.metrics) && sc.metrics.length) {
|
| 332 |
+
h += `<section class="val-sec"><h4>Fundamentals scorecard — ${sc.total}/${sc.max}</h4><div class="val-scroll"><table class="val-t"><tr><th>Area</th><th>Metric</th><th>Value</th><th>Read</th></tr>
|
| 333 |
+
${sc.metrics.map(m => `<tr><td class="k">${escapeHtml(m.cat || "")}</td><td>${escapeHtml(m.name || "")}</td><td>${escapeHtml(String(m.value ?? ""))}</td><td class="${/STRONG|GOOD/.test(m.rating) ? "up" : /WEAK|BAD/.test(m.rating) ? "down" : "flat"}">${escapeHtml(String(m.rating || "").toLowerCase())}</td></tr>`).join("")}</table></div></section>`;
|
| 334 |
+
}
|
| 335 |
+
if (dg.summary && !/No AI API key/.test(dg.summary)) {
|
| 336 |
+
h += `<section class="val-sec"><h4>Engine summary</h4><div class="val-note" style="white-space:pre-wrap">${escapeHtml(dg.summary).slice(0, 2500)}</div></section>`;
|
| 337 |
+
}
|
| 338 |
+
root.innerHTML = h;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
// "How this number is built" — every source in the final blend with its effective share.
|
| 342 |
function renderBlendPanel(result) {
|
| 343 |
const host = $("#valuation"); if (!host) return;
|
|
|
|
| 483 |
document.querySelectorAll(".tab-page").forEach(p => p.classList.toggle("active", p.id === `tab-${name}`));
|
| 484 |
if (name === "log") { const l = $("#log"); l.scrollTop = l.scrollHeight; }
|
| 485 |
}
|
| 486 |
+
$("#tabbar").addEventListener("click", e => {
|
| 487 |
+
const b = e.target.closest(".tab-btn"); if (!b) return;
|
| 488 |
+
if (b.id === "print-btn") { document.querySelectorAll(".tab-page").forEach(p => p.classList.add("active")); window.print(); setTimeout(() => setTab("debate"), 500); return; }
|
| 489 |
+
setTab(b.dataset.tab);
|
| 490 |
+
});
|
| 491 |
|
| 492 |
/* ---------- key-numbers strip ---------- */
|
| 493 |
function renderKeyStrip(result) {
|
|
|
|
| 656 |
renderReconcile(result);
|
| 657 |
renderValuationTab(result);
|
| 658 |
renderBlendPanel(result);
|
| 659 |
+
renderInstitutional(result);
|
| 660 |
+
renderDiagnostics(result);
|
| 661 |
$("#tabbar .tab-btn[data-tab='valuation']")?.classList.add("ready");
|
| 662 |
const d = result.debate; if (!d) return;
|
| 663 |
fillBubble("neutral", "opening", d.roles.neutral);
|
|
|
|
| 776 |
$("#keystrip").hidden = true; $("#keystrip").innerHTML = "";
|
| 777 |
$("#verdict-reconcile").hidden = true; $("#verdict-reconcile").innerHTML = "";
|
| 778 |
$("#valuation").innerHTML = '<p class="placeholder">The Damodaran valuation appears when the run completes.</p>';
|
| 779 |
+
$("#diagnostics").innerHTML = '<p class="placeholder">Data provenance, risk profile and model diagnostics appear when the run completes.</p>';
|
| 780 |
$("#tabbar .tab-btn[data-tab='valuation']")?.classList.remove("ready");
|
| 781 |
setTab("debate");
|
| 782 |
initTimeline();
|