Initial commit: CI Agent competitive-intelligence monitoring app

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).
This commit is contained in:
2026-08-05 10:48:20 -04:00
commit 1a4c80958f
365 changed files with 43541 additions and 0 deletions
+249
View File
@@ -0,0 +1,249 @@
"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>
);
}