FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
250 lines
8.9 KiB
TypeScript
250 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import type { ConfidenceLabel, Finding, InferredProject, ReportResponse } from "@/lib/types";
|
|
import { formatDateTime } from "@/lib/format";
|
|
|
|
const CONFIDENCE_LABELS: Record<ConfidenceLabel, string> = {
|
|
confirmed: "Confirmed",
|
|
strongly_indicated: "Strongly indicated",
|
|
likely: "Likely",
|
|
possible: "Possible",
|
|
unconfirmed: "Unconfirmed",
|
|
insufficient_evidence: "Insufficient evidence",
|
|
};
|
|
|
|
const CONFIDENCE_CLASSES: Record<ConfidenceLabel, string> = {
|
|
confirmed: "bg-green-100 text-green-800",
|
|
strongly_indicated: "bg-green-50 text-green-700",
|
|
likely: "bg-amber-100 text-amber-800",
|
|
possible: "bg-amber-50 text-amber-700",
|
|
unconfirmed: "bg-slate-100 text-slate-600",
|
|
insufficient_evidence: "bg-slate-100 text-slate-500",
|
|
};
|
|
|
|
function ConfidenceBadge({ confidence }: { confidence: ConfidenceLabel }) {
|
|
return (
|
|
<span
|
|
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${CONFIDENCE_CLASSES[confidence]}`}
|
|
>
|
|
{CONFIDENCE_LABELS[confidence]}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function Section({
|
|
number,
|
|
title,
|
|
children,
|
|
}: {
|
|
number: number;
|
|
title: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<section className="border-b border-slate-200 py-6 first:pt-0 last:border-0">
|
|
<h2 className="text-sm font-semibold uppercase tracking-wide text-slate-500">
|
|
{number}. {title}
|
|
</h2>
|
|
<div className="mt-3 text-sm text-slate-700">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function FindingList({ findings }: { findings: Finding[] }) {
|
|
if (findings.length === 0) {
|
|
return (
|
|
<p className="text-sm text-slate-500">
|
|
No findings for this section from the current evidence.
|
|
</p>
|
|
);
|
|
}
|
|
return (
|
|
<ul className="space-y-3">
|
|
{findings.map((f, i) => (
|
|
<li key={i} className="rounded-md border border-slate-200 p-3">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className="font-medium text-slate-900">{f.headline}</p>
|
|
<ConfidenceBadge confidence={f.confidence} />
|
|
</div>
|
|
<p className="mt-1 text-slate-600">{f.summary}</p>
|
|
{f.date && <p className="mt-1 text-xs text-slate-400">{f.date}</p>}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
function ProjectList({ projects }: { projects: InferredProject[] }) {
|
|
if (projects.length === 0) {
|
|
return (
|
|
<p className="text-sm text-slate-500">
|
|
No inferred strategic projects from the current evidence.
|
|
</p>
|
|
);
|
|
}
|
|
return (
|
|
<ul className="space-y-3">
|
|
{projects.map((p, i) => (
|
|
<li key={i} className="rounded-md border border-slate-200 p-3">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className="font-medium text-slate-900">{p.project_name}</p>
|
|
<ConfidenceBadge confidence={p.status} />
|
|
</div>
|
|
<p className="mt-1 text-slate-600">{p.summary}</p>
|
|
{p.alternative_explanations.length > 0 && (
|
|
<p className="mt-1 text-xs text-slate-500">
|
|
Alternative explanations: {p.alternative_explanations.join("; ")}
|
|
</p>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
function StringList({ items, emptyText }: { items: string[]; emptyText: string }) {
|
|
if (items.length === 0) return <p className="text-sm text-slate-500">{emptyText}</p>;
|
|
return (
|
|
<ul className="list-inside list-disc space-y-1">
|
|
{items.map((item, i) => (
|
|
<li key={i}>{item}</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
export function ReportView({ report }: { report: ReportResponse }) {
|
|
const [copied, setCopied] = useState(false);
|
|
const c = report.structured_report;
|
|
|
|
const handleCopy = async () => {
|
|
await navigator.clipboard.writeText(JSON.stringify(c, null, 2));
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
const downloadJson = () => {
|
|
const blob = new Blob([JSON.stringify(c, null, 2)], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `${report.title}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex flex-wrap items-center justify-between gap-3 print:hidden">
|
|
<div>
|
|
<h1 className="text-xl font-semibold text-slate-900">{report.title}</h1>
|
|
<p className="text-xs text-slate-500">
|
|
{report.report_type} report · generated {formatDateTime(report.created_at)} ·{" "}
|
|
{report.model_provider}/{report.model_name}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={handleCopy}
|
|
className="focus-ring rounded-md border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50"
|
|
>
|
|
{copied ? "Copied!" : "Copy JSON"}
|
|
</button>
|
|
<button
|
|
onClick={() => window.print()}
|
|
className="focus-ring rounded-md border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50"
|
|
>
|
|
Print
|
|
</button>
|
|
<button
|
|
onClick={downloadJson}
|
|
className="focus-ring rounded-md border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50"
|
|
>
|
|
Export JSON
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 rounded-lg border border-slate-200 bg-white p-6">
|
|
<Section number={1} title="Executive Summary">
|
|
<p>{c.executive_summary}</p>
|
|
</Section>
|
|
<Section number={2} title="Company Overview">
|
|
<p>{c.company_overview}</p>
|
|
</Section>
|
|
<Section number={3} title="Products and Service Landscape">
|
|
<FindingList findings={c.products_and_services} />
|
|
</Section>
|
|
<Section number={4} title="Recent Developments">
|
|
<FindingList findings={c.recent_developments} />
|
|
</Section>
|
|
<Section number={5} title="Strategic Initiatives">
|
|
<FindingList findings={c.strategic_initiatives} />
|
|
</Section>
|
|
<Section number={6} title="Key Project Signals">
|
|
<ProjectList projects={c.key_inferred_projects} />
|
|
</Section>
|
|
<Section number={7} title="Competitive Positioning">
|
|
<p>{c.market_positioning}</p>
|
|
<p className="mt-2">{c.competitor_comparison}</p>
|
|
</Section>
|
|
<Section number={8} title="SWOT Analysis">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-500">Strengths</h3>
|
|
<StringList items={c.swot.strengths} emptyText="None noted." />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-500">Weaknesses</h3>
|
|
<StringList items={c.swot.weaknesses} emptyText="None noted." />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-500">Opportunities</h3>
|
|
<StringList items={c.swot.opportunities} emptyText="None noted." />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-500">Threats</h3>
|
|
<StringList items={c.swot.threats} emptyText="None noted." />
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
<Section number={9} title="Hiring Signals">
|
|
<FindingList findings={c.hiring_signals} />
|
|
</Section>
|
|
<Section number={10} title="Product and Technology Signals">
|
|
<FindingList findings={[...c.technology_signals, ...c.patent_signals]} />
|
|
</Section>
|
|
<Section number={11} title="Customer Sentiment">
|
|
<p>{c.customer_sentiment}</p>
|
|
</Section>
|
|
<Section number={12} title="Financial and Regulatory Signals">
|
|
<FindingList findings={[...c.financial_signals, ...c.regulatory_and_legal_signals]} />
|
|
</Section>
|
|
<Section number={13} title="Risks and Opportunities">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-500">Risks</h3>
|
|
<StringList items={c.risks} emptyText="None noted." />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-500">Opportunities</h3>
|
|
<StringList items={c.opportunities} emptyText="None noted." />
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
<Section number={14} title="Important Unknowns">
|
|
<StringList items={c.unknowns_and_missing_data} emptyText="None noted." />
|
|
</Section>
|
|
<Section number={15} title="Monitoring Recommendations">
|
|
<StringList items={c.monitoring_recommendations} emptyText="None noted." />
|
|
</Section>
|
|
<Section number={16} title="Methodology and Limitations">
|
|
<p>{c.methodology}</p>
|
|
<p className="mt-2 text-slate-500">{c.limitations}</p>
|
|
</Section>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|