s-ttp commited on
Commit
7ef63c6
·
verified ·
1 Parent(s): b1aab54

Upload static/app.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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 => { const b = e.target.closest(".tab-btn"); if (b) setTab(b.dataset.tab); });
 
 
 
 
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 &amp; 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&amp;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 &amp; 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();