/* global React, SparkTrj, StripTrj, Citation, Discussions, ChatPanel */ // pages.jsx — Homepage (search-first), StockDetail (PRODUCT.md order), // RequestStock (uncovered ticker capture). // Exposes window.Homepage, window.StockDetail, window.RequestStock. const { useState, useEffect, useMemo, useRef } = React; // Tiny markdown-bold renderer: splits on **…** and renders . function renderBold(text) { if (!text) return null; const parts = String(text).split(/(\*\*[^*]+\*\*)/g); return parts.map((p, i) => p.startsWith("**") && p.endsWith("**") ? {p.slice(2, -2)} : {p} ); } // Inline "↗ Ask the agent" link — pre-fills + opens the floating chat panel. // Bridges the durable brief to the conversational surface via a custom event. function AskAgentLink({ prefill, children }) { function onClick(e) { e.preventDefault(); window.dispatchEvent(new CustomEvent("cct:ask-agent", { detail: { prefill, autosend: true } })); } return ( ); } // Sources strip — one chip per quarter linking to the source PDF, plus an // "All filings" fallback chip. Closes the trust loop the methodology page // describes: any quote → original document in one click. function SourcesStrip({ ticker, quarters }) { const perQ = (window.CCT_SOURCE_URLS || {})[ticker] || {}; const fallback = (window.CCT_IR_FALLBACK || {})[ticker]; const qList = (quarters && quarters.length ? quarters : Object.keys(perQ)).slice(); // Don't render an empty strip — happens if both source map and fallback miss. if (qList.every(q => !perQ[q]) && !fallback) return null; return (
Sources {qList.map(q => { const url = perQ[q]; if (url) { return ( {q} PDF ↗ ); } return null; })} {fallback && ( All filings IR site ↗ )}
); } // "Flag this finding" — small link at the bottom of each finding card. // Encodes {subject, url} as base64 JSON in the methodology hash so the form // pre-fills. Stable contract: MethodologyPage decodes the same shape. function FlagFindingLink({ ticker, kind, topic }) { function onClick(e) { e.preventDefault(); const subject = `${ticker} · ${kind}: ${topic || "(unspecified)"}`.slice(0, 140); const url = location.origin + location.pathname + `#stock:${ticker}`; const payload = { subject, url, ticker, kind, topic }; const flag = encodeURIComponent(btoa(JSON.stringify(payload))); location.hash = `methodology?flag=${flag}`; window.scrollTo({ top: 0, behavior: "instant" }); } return ( ); } // ============================================================================ // Search — covered tickers vs uncovered → request flow // ============================================================================ const COVERED_INDEX = (() => { return (window.CCT_DATA?.stocks || []).map(s => ({ ticker: s.ticker, company: s.company, sector: s.sector, drift: window.CCT_ONELINE?.[s.ticker] || s.drift_summary })); })(); function searchCovered(q) { const t = q.trim().toLowerCase(); if (!t) return []; return COVERED_INDEX.filter(s => s.ticker.toLowerCase().includes(t) || s.company.toLowerCase().includes(t) ); } function SearchBar({ navigate, autoFocus }) { const [q, setQ] = useState(""); const [active, setActive] = useState(0); const inputRef = useRef(null); const results = useMemo(() => searchCovered(q), [q]); useEffect(() => { if (autoFocus && inputRef.current) inputRef.current.focus(); }, [autoFocus]); function submit(picked) { const choice = picked || results[active]; if (choice) { navigate(`stock:${choice.ticker}`); } else if (q.trim()) { navigate(`request:${encodeURIComponent(q.trim())}`); } } function onKey(e) { if (e.key === "ArrowDown") { e.preventDefault(); setActive(a => Math.min(results.length - 1, a + 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActive(a => Math.max(0, a - 1)); } else if (e.key === "Enter") { e.preventDefault(); submit(); } else if (e.key === "Escape") { setQ(""); } } return (
{ setQ(e.target.value); setActive(0); }} onKeyDown={onKey} placeholder="Search a stock — NSE / BSE ticker or company name" spellCheck={false} autoComplete="off" aria-label="Search covered stocks" />
{q.trim() && (
{results.length > 0 && results.map((r, i) => ( ))}
)}
{COVERED_INDEX.length} stocks covered · request the {COVERED_INDEX.length + 1}th
); } // ============================================================================ // Homepage — search bar, cross-stock patterns, covered-companies list // ============================================================================ function Homepage({ navigate }) { const data = window.CCT_DATA; const patterns = window.CCT_PATTERNS || []; return (
Throughline · Issue 01 · {data.latest_quarter || "Q3 FY26"}

What management is saying.
And what they stopped saying.

{/* Cross-stock patterns — three sentences */}
Cross-stock — visible only when you read all seven
    {patterns.map((p, i) => (
  1. 0{i + 1}

    {p.title}

    {p.body}

    {p.tags.map(t => ( ))}
  2. ))}
{/* Covered companies — one-line drift each */}
Covered companies · {data.tickers_complete} of {data.tickers_total}
latest · {data.latest_quarter || "Q3 FY26"}
    {data.stocks.map(s => (
  • ))}
Pipeline Next batch · {(data.deferred || []).join(" · ")}
); } // ============================================================================ // Request stock — uncovered ticker capture // ============================================================================ function RequestStock({ query, navigate }) { const decoded = decodeURIComponent(query || ""); const [email, setEmail] = useState(""); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); async function submit(e) { e.preventDefault(); setError(null); if (window.CCT_USE_REAL_API) { try { const base = window.CCT_API_BASE || ""; const res = await fetch(`${base}/api/requests`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ query: decoded, email }) }); if (!res.ok) throw new Error(`request ${res.status}`); setSubmitted(true); return; } catch (err) { // Fall through to local persistence so the user's signal is never lost. setError("Couldn't reach the server — saved your request locally; we'll pick it up next sync."); } } try { const k = "cct.requests.v1"; const all = JSON.parse(localStorage.getItem(k) || "[]"); all.push({ query: decoded, email, ts: new Date().toISOString() }); localStorage.setItem(k, JSON.stringify(all)); } catch {} setSubmitted(true); } return (
/ Request
Not yet covered

We don't cover {decoded} yet.

Throughline covers {COVERED_INDEX.length} Indian midcaps today, growing to 25-50. Coverage expands based on real demand — your request goes into a signal queue we read every week.

{submitted ? (
✓ Logged

We'll email {email} when {decoded} goes live.
Stored locally for now — wired to POST /api/requests when the backend lands.

) : (
No marketing. One email when {decoded} ships.
{error && (
{error}
)}
)}
Already covered
    {COVERED_INDEX.slice(0, 7).map(s => (
  • ))}
); } // ============================================================================ // Stock page — PRODUCT.md section order // Header → Drift → What's new → Walked back → People & gov → Dodged → // Analyst coverage → Chat (right rail) → Discussions (bottom) // ============================================================================ // Light internal helpers function SignalChip({ kind, label }) { return ( {label} ); } function SeverityTag({ level }) { if (!level) return null; return ( {level} ); } function NewItem({ n, kind, fullQ, ticker }) { if (!n || !n.topic) return null; const ck = kind === "emerged" && n.topic.startsWith("AssistX") ? "PERSISTENT_Q3FY26_or_assistx" : kind === "disappeared" && n.topic.startsWith("Macro") ? "PERSISTENT_Q2FY26_qa_macro" : null; const askPrefill = kind === "emerged" && n.topic.startsWith("AssistX") ? "Walk me through the AssistX Customer Zero pivot" : null; return (

{n.topic}

"{n.quote || n.peak_quote}"
{n.significance}
{ck && ( )} {askPrefill && Ask the agent about this}
); } function WalkedBackItem({ g, ticker }) { const featured = g.severity === "MATERIAL"; const askPrefill = featured && /ebit|margin/i.test(g.metric || "") ? "Walk me through the EBIT margin walk-back" : null; return (

{g.metric}

{g.from_q} → {g.to_q}
Was · {g.from_q}
{g.from}
Now · {g.to_q}
{g.to}
{g.note && (

{g.note}

)} {g.quote && (
"{g.quote}"
)}
{askPrefill && Ask the agent about this}
); } function GovernanceItem({ g, ticker }) { return (
{g.kind}
{g.who}
{g.quarter}
{g.detail}
{g.quote &&
"{g.quote}"
}
); } function DodgedItem({ d, ticker }) { const ck = d.quarter === "Q3FY26" && /margin/i.test(d.question) ? "PERSISTENT_Q3FY26_qa_margin" : null; return (
{d.quarter} · {d.analyst}

"{d.question}"

Answer (excerpt)
"{d.answer}"
Why it's a dodge
{d.reason}
Why it matters
{renderBold(d.why_it_matters)}
{ck && }
); } function AnalystCoverageBlock({ analysts, latest }) { const list = analysts[latest] || []; const newOnes = analysts.new || []; const dropped = analysts.dropped || []; return (
{latest} coverage · {list.length} analysts
{list.map(a => { const isNew = newOnes.includes(a); return (
{a} {isNew && + new}
); })}
+ Joined
{newOnes.length ? newOnes.join(", ") : "—"}
− Went silent
{dropped.length ? dropped.join(", ") : "—"}
); } // Main stock-page component function StockDetail({ ticker, navigate }) { // PERSISTENT has the full hi-fi block; others use CCT_STOCK_DETAIL scaffold. const isPersistent = ticker === "PERSISTENT"; const D = isPersistent ? window.CCT_PERSISTENT : null; const S = !isPersistent ? window.CCT_STOCK_DETAIL?.[ticker] : null; const stub = COVERED_INDEX.find(s => s.ticker === ticker); if (!D && !S && !stub) { // Treat as request flow return ; } const company = D ? D.company : (S?.company || stub?.company); const sector = D ? D.sector : (S?.sector || stub?.sector); const latest = D ? D.latest_quarter : (S?.latest_quarter || "Q3FY26"); const fullQ = D ? D.comparison_quarters : ["Q4FY25","Q1FY26","Q2FY26","Q3FY26"]; const headline = D ? D.headline : (S?.headline || stub?.drift); const whatsNew = D ? [...(D.emerged || []).map(e => ({ ...e, _kind: "emerged" })), ...(D.disappeared || []).map(e => ({ ...e, _kind: "disappeared" }))] : [ ...((S?.emerged || S?.whats_new || []).map(e => ({ ...e, _kind: "emerged" }))), ...((S?.disappeared || []).map(e => ({ ...e, _kind: "disappeared" }))), ]; const walkedBack = D ? (D.guidance_changes || []) : (S?.guidance_changes || S?.walked_back || []); const governance = D ? (window.CCT_GOVERNANCE?.[ticker] || []) : (S?.governance || window.CCT_GOVERNANCE?.[ticker] || []); const dodged = D ? (D.dodged || []) : (S?.dodged || []); const analysts = D ? D.analysts : (S?.analysts || { [latest]: [], new: [], dropped: [] }); return (
{/* Left column — the brief */}
{/* §1 Header */}
/ {ticker}
{ticker} {company} {sector}
Latest quarter analyzed · {latest}
{/* §2 Drift summary — display serif, prominent (coral bar marks pull-quote) */}
Drift summary · {latest}

{headline}

{/* §2b What this implies — renders for any stock with implications. */} {(() => { const src = D ?? S; if (!src?.implications || src.implications.length === 0) return null; return (
What this implies for the thesis
{src.implications_audience && (

{src.implications_audience}

)}
    {src.implications.map((line, i) => (
  • {line}
  • ))}
); })()} {/* §3 What's new this quarter */}
§ 01
What's new this quarter

Topics they started — and stopped — talking about.

{whatsNew.length === 0 ? (
[ — ]
No emergent narratives this quarter.
) : whatsNew.map((n, i) => )}
{/* §4 What management walked back — visually distinct */}
§ 02
What management walked back

Goalposts that moved — quietly.

Specific commitments management made, then retreated from. From-value, to-value, the verbatim quote, the quarter where it shifted.

{walkedBack.length === 0 ? (
[ — ]
No guidance walk-backs this quarter.
) : walkedBack.map((g, i) => )}
{/* §5 People & governance — only renders when non-empty */} {governance.length > 0 && (
§ 03
People & governance

Leadership moves that matter.

{governance.map((g, i) => )}
)} {/* §6 What got dodged */}
§ {governance.length > 0 ? "04" : "03"}
What got dodged

Questions where management gave non-answers.

{dodged.length === 0 ? (
[ — ]
No flagged dodges this quarter.
) : dodged.map((d, i) => )}
{/* §7 Analyst coverage */}
§ {governance.length > 0 ? "05" : "04"}
Analyst coverage

Who's showing up to ask.

{/* §9 Discussions — bottom of page */}
§ {governance.length > 0 ? "06" : "05"}
Discussions

Other investors on this stock.

{/* Floating chat — collapsed pill bottom-right; expands into a panel. Lives outside .stock-grid so it stays anchored to the viewport, not the brief column. */}
); } Object.assign(window, { Homepage, StockDetail, RequestStock, SearchBar });