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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user