import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { CloudEquityDiagnosticCoverage, CloudEquityDiagnosticEvidence, CloudEquityDiagnosticFinding, CloudEquityDiagnosticFindingKind, CloudEquityDiagnosticMode, CloudEquityDiagnosticResponse, } from "../../../api-client"; import { apiClient } from "../../../api-client"; import { ApiRequestError } from "../../../api-client/errors"; import { Button, ChoiceDialog, EmptyState, PaneStatusBody, SectionHeading, Spinner, usePaneFooter, usePaneMenuItems } from "../../../components"; import { ExternalLinkText, PaneLinkMenu } from "../../../components/ui"; import { t, tf } from "../../../i18n"; import { useShortcut } from "../../../react/input"; import { colors } from "../../../theme/colors"; import { Box, ScrollBox, Text, TextAttributes, useRendererHost, useUiCapabilities } from "../../../ui"; import { useOptionalDialog, type PromptContext } from "../../../ui/dialog"; import { formatTimeAgo, truncateToDisplayWidth } from "../../../utils/format"; import { isPlainKey } from "../../../utils/keyboard"; import { SignInWall } from "../cloud/auth-actions"; import { useCloudPlanAction, useCloudUpgradeAction } from "../shared/cloud-upgrade"; import { usePlanAccess } from "../shared/plan-access"; import { useBoundTicker } from "../shared/ticker-request"; const FOOTER_ID = "equity-diagnostic"; const REFRESH_SCOPE = "equity-diagnostic:refresh"; /** Width of the "Observed"/"Reading" label column before the layout stacks. */ const LABEL_WIDTH = 10; const STACK_BELOW_WIDTH = 44; const COVERAGE_LABEL_WIDTH = 16; const LOADING_STEPS = [ "Market data", "SEC EDGAR filings", "FINRA short interest", "News", "Reviewing evidence", ] as const; interface DiagnosticFailure { status?: number; message: string; } function toFailure(error: unknown): DiagnosticFailure { return { status: error instanceof ApiRequestError ? error.status : undefined, message: error instanceof Error ? error.message : String(error), }; } function failureText(failure: DiagnosticFailure): string { if (failure.status === 429) return t("Rate limited. Try again in a moment."); if (failure.status != null && failure.status >= 500) return t("Diagnostic service is unavailable."); return failure.message; } /** * One report request per visited surface. A failed refresh keeps the report the * user is already reading, so a rate limit or an outage never blanks the pane. */ function useEquityDiagnostic(symbol: string | null, exchange: string, enabled: boolean) { const [state, setState] = useState<{ report: CloudEquityDiagnosticResponse | null; loading: boolean; loadingStep: number; failure: DiagnosticFailure | null; }>({ report: null, loading: false, loadingStep: 0, failure: null }); const generationRef = useRef(0); const pollTimerRef = useRef | null>(null); const clearPoll = useCallback(() => { if (pollTimerRef.current) clearTimeout(pollTimerRef.current); pollTimerRef.current = null; }, []); const load = useCallback((mode: CloudEquityDiagnosticMode) => { if (!symbol || !enabled) return; clearPoll(); generationRef.current += 1; const generation = generationRef.current; setState((current) => ({ ...current, loading: true, loadingStep: 1, failure: null })); const request = (nextMode: CloudEquityDiagnosticMode) => { apiClient.getCloudEquityDiagnostic(symbol, exchange || undefined, nextMode) .then((result) => { if (generationRef.current !== generation) return; if (result.status === "generating") { setState((current) => ({ ...current, loadingStep: Math.min(LOADING_STEPS.length, current.loadingStep + 1), })); const retryAfterMs = Math.max(10, Math.min(5_000, result.retryAfterMs)); pollTimerRef.current = setTimeout(() => request("cache-first"), retryAfterMs); return; } clearPoll(); setState({ report: result, loading: false, loadingStep: 0, failure: null }); }) .catch((error: unknown) => { if (generationRef.current !== generation) return; clearPoll(); setState((current) => ({ report: current.report, loading: false, loadingStep: 0, failure: toFailure(error), })); }); }; request(mode); }, [clearPoll, enabled, exchange, symbol]); useEffect(() => { // A report belongs to one company, so drop it rather than show it under the next. generationRef.current += 1; clearPoll(); setState({ report: null, loading: false, loadingStep: 0, failure: null }); load("cache-first"); return clearPoll; }, [clearPoll, load]); return { ...state, load }; } function verdictLabel(verdict: CloudEquityDiagnosticResponse["verdict"]): string { switch (verdict) { case "risk_skewed": return t("Risk skewed"); case "opportunity_skewed": return t("Opportunity skewed"); case "balanced": return t("Balanced"); default: return t("Unclear"); } } function verdictColor(verdict: CloudEquityDiagnosticResponse["verdict"]): string { switch (verdict) { case "risk_skewed": return colors.negative; case "opportunity_skewed": return colors.positive; case "balanced": return colors.text; default: return colors.textDim; } } function findingColor(kind: CloudEquityDiagnosticFindingKind): string { switch (kind) { case "red_flag": return colors.negative; case "green_flag": return colors.positive; default: return colors.warning; } } function severityLabel(severity: CloudEquityDiagnosticFinding["severity"]): string { if (severity >= 3) return t("HIGH"); if (severity === 2) return t("MEDIUM"); return t("LOW"); } function coverageColor(status: CloudEquityDiagnosticCoverage["status"]): string { if (status === "available") return colors.textDim; return status === "failed" ? colors.warning : colors.textMuted; } function coverageLabel(status: CloudEquityDiagnosticCoverage["status"]): string { switch (status) { case "available": return t("available"); case "no_data": return t("no data"); case "unsupported": return t("unsupported"); default: return t("failed"); } } function percent(value: number): string { const scaled = value <= 1 ? value * 100 : value; return `${Math.round(scaled)}%`; } function citationLabel(evidence: CloudEquityDiagnosticEvidence): string { const date = evidence.asOf?.slice(0, 10); return date && !evidence.label.includes(date) ? `${evidence.label} ${date}` : evidence.label; } function datasetLabel(dataset: string): string { return dataset.replaceAll("_", " "); } function sortFindings(findings: readonly CloudEquityDiagnosticFinding[]): CloudEquityDiagnosticFinding[] { return [...findings].sort((left, right) => ( right.severity - left.severity || right.confidence - left.confidence )); } const FINDING_SECTION_ORDER: readonly CloudEquityDiagnosticFindingKind[] = ["red_flag", "anomaly", "green_flag"]; interface CitedSource { id: string; label: string; finding: string; url: string; } /** * The linked evidence behind the findings on screen, in reading order: the * preview's one finding, or every section of the full report. */ function citedSources(report: CloudEquityDiagnosticResponse): CitedSource[] { const evidenceById = new Map(report.evidence.map((evidence) => [evidence.id, evidence])); const sorted = sortFindings(report.findings); const findings = report.access === "preview" ? report.findings.slice(0, 1) : FINDING_SECTION_ORDER.flatMap((kind) => sorted.filter((finding) => finding.kind === kind)); const sources: CitedSource[] = []; const seen = new Set(); for (const finding of findings) { for (const evidenceId of finding.evidenceIds) { const evidence = evidenceById.get(evidenceId); if (!evidence?.url || seen.has(evidence.id)) continue; seen.add(evidence.id); sources.push({ id: evidence.id, label: citationLabel(evidence), finding: finding.title, url: evidence.url }); } } return sources; } function Paragraph({ text: value, width, color, bold }: { text: string; width: number; color: string; bold?: boolean; }) { return ( {value} ); } function DiagnosticLoading({ step }: { step: number }) { const visible = LOADING_STEPS.slice(0, Math.max(1, step)); return ( {visible.slice(0, -1).map((label) => ( {t(label)} ))} ); } /** Label plus body text, stacked instead of columned once the pane gets narrow. */ function LabeledText({ label, text: value, width, color }: { label: string; text: string; width: number; color: string; }) { if (width < STACK_BELOW_WIDTH) { return ( {t(label)} ); } return ( {t(label)} ); } function FindingView({ finding, evidenceById, width }: { finding: CloudEquityDiagnosticFinding; evidenceById: Map; width: number; }) { const kindColor = findingColor(finding.kind); const citations = finding.evidenceIds .map((id) => evidenceById.get(id)) .filter((evidence): evidence is CloudEquityDiagnosticEvidence => !!evidence); return ( {severityLabel(finding.severity)} {tf("{value} confidence", { value: percent(finding.confidence) })} {citations.map((evidence) => ( {" · "} {evidence.url ? : {citationLabel(evidence)}} ))} ); } function FindingSection({ heading, findings, evidenceById, width }: { heading: string; findings: CloudEquityDiagnosticFinding[]; evidenceById: Map; width: number; }) { if (findings.length === 0) return null; return ( {findings.map((finding) => ( ))} ); } function CoverageSection({ coverage, width }: { coverage: CloudEquityDiagnosticCoverage[]; width: number; }) { if (coverage.length === 0) return null; const detailWidth = Math.max(12, width - COVERAGE_LABEL_WIDTH); return ( {coverage.map((entry) => { const detail = [ coverageLabel(entry.status), entry.asOf?.slice(0, 10), entry.note, ] .filter(Boolean) .join(" · "); return ( {truncateToDisplayWidth(datasetLabel(entry.dataset), COVERAGE_LABEL_WIDTH - 1)} ); })} ); } function ReportView({ report, width }: { report: CloudEquityDiagnosticResponse; width: number; }) { const evidenceById = useMemo( () => new Map(report.evidence.map((evidence) => [evidence.id, evidence])), [report.evidence], ); const sorted = useMemo(() => sortFindings(report.findings), [report.findings]); const byKind = (kind: CloudEquityDiagnosticFindingKind) => sorted.filter((finding) => finding.kind === kind); // When the report was generated is status, so it lives in the footer. const meta = [ report.companyName, tf("confidence {value}", { value: percent(report.confidence) }), ].filter(Boolean).join(" · "); // Every citation is also a pane menu entry, so the report's sources open // without a pointer. return ( {verdictLabel(report.verdict)} {report.status === "insufficient_data" ? : } {report.watchItems.length > 0 && ( {report.watchItems.map((item, index) => ( {"· "} ))} )} ); } function PreviewReportView({ report, width, onUpgrade, onPlan }: { report: CloudEquityDiagnosticResponse; width: number; onUpgrade: () => void; onPlan: () => void; }) { const evidenceById = useMemo( () => new Map(report.evidence.map((evidence) => [evidence.id, evidence])), [report.evidence], ); const finding = report.findings[0]; const meta = [ report.companyName, t("Free preview"), ].filter(Boolean).join(" · "); return ( {finding ? : }