"use client"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { Suspense, use, useState } from "react"; import { AlertTriangle, Check, CheckCheck, ChevronDown, ChevronRight, Loader2, Plus, RefreshCw, } from "lucide-react"; import { CompanyPillLink } from "@/components/ui/company-pill"; import { CopyableEmail } from "@/components/ui/copyable-email"; import { CompanyStatusBadge, SeverityBadge } from "@/components/ui/badge"; import { NotificationChannelBox } from "@/components/ui/notification-channel-box"; import { Select } from "@/components/ui/select"; import { useAlerts, useMarkAlertRead, useResolveAlert } from "@/hooks/use-alerts"; import { useSystemStatus } from "@/hooks/use-auth"; import { useCompanies, useCompany, useDeleteCompany, useRefreshEnrichmentSection, useSetCompanyPaused, useUpdateMonitorConfiguration, } from "@/hooks/use-companies"; import { useNotificationDestinations } from "@/hooks/use-notification-destinations"; import { useCreateSource, useDeleteSource, useSources, useTestSource, useUpdateSource, } from "@/hooks/use-sources"; import { useCompanyRuns, useRunCompanyNow } from "@/hooks/use-monitoring-runs"; import { useCompanyReports, useGenerateReport } from "@/hooks/use-reports"; import { useSnapshots } from "@/hooks/use-snapshots"; import type { EnrichmentSection, MonitoringFrequency } from "@/lib/types"; import { FREQUENCY_LABELS, MONITORING_FREQUENCIES, SEVERITY_LABELS, SEVERITY_LEVELS, } from "@/lib/types"; import { companyNameFromUrl, formatDateTime, formatEnrichmentReason, formatMoney, formatRelative, summarizeEnrichmentError, } from "@/lib/format"; const TABS = [ "Overview", "Enrichment", "Latest report", "Alerts", "Sources", "Monitoring history", "Snapshots", "Configuration", ] as const; type Tab = (typeof TABS)[number]; const TAB_DESCRIPTIONS: Record = { Overview: "A snapshot of this company — description, competitors, key details, and its monitoring schedule.", Enrichment: "Rich company data — funding, leadership, products, and customers.", "Latest report": "The most recent AI-generated competitive intelligence report, grounded in everything collected so far.", Alerts: "Notable changes detected for this company, ranked by severity, with actions to mark them read or resolved.", Sources: "Every data source being monitored for this company, and how often each one is checked.", "Monitoring history": "A log of every monitoring run for this company — what it collected and whether it succeeded.", Snapshots: "The raw content collected from each source on each run, so you can see exactly what was captured.", Configuration: "Scheduling, severity threshold, and notification settings for this company.", }; function charCount(text: string | null): string { return `${(text ?? "").length.toLocaleString()} chars`; } const RUN_STATUS_CLASSES: Record = { queued: "bg-slate-100 text-slate-700", running: "bg-blue-100 text-blue-700", successful: "bg-green-100 text-green-700", partial: "bg-amber-100 text-amber-700", failed: "bg-red-100 text-red-700", }; const ERROR_TONE_CLASSES = { amber: { box: "border-amber-200 bg-amber-50 text-amber-800", button: "border-amber-300 text-amber-800 hover:bg-amber-100", }, red: { box: "border-red-200 bg-red-50 text-red-800", button: "border-red-300 text-red-800 hover:bg-red-100", }, } as const; function EnrichmentErrorSummary({ tone, summary, errors, }: { tone: keyof typeof ERROR_TONE_CLASSES; summary: string; errors: Record; }) { const [expanded, setExpanded] = useState(false); const sections = Object.entries(errors); const classes = ERROR_TONE_CLASSES[tone]; return (

{summary}

{sections.length > 0 && ( )}
    {sections.map(([section, raw]) => (
  • {section}

    {summarizeEnrichmentError(raw)}

    {raw}
  • ))}
); } function RefreshSectionButton({ section, label, mutation, }: { section: EnrichmentSection; label: string; mutation: ReturnType; }) { const isRefreshing = mutation.isPending && mutation.variables === section; return ( ); } export default function CompanyDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); return ( Loading…

}>
); } function CompanyDetailPageInner({ id }: { id: string }) { const router = useRouter(); const searchParams = useSearchParams(); const requestedTab = searchParams.get("tab"); const initialTab = (TABS as readonly string[]).includes(requestedTab ?? "") ? (requestedTab as Tab) : "Overview"; const { data: company, isLoading } = useCompany(id); const { data: allCompanies } = useCompanies(); const setPaused = useSetCompanyPaused(); const deleteCompany = useDeleteCompany(); const updateMonitor = useUpdateMonitorConfiguration(id); const { data: sources, isLoading: sourcesLoading } = useSources(id); const createSource = useCreateSource(id); const deleteSource = useDeleteSource(id); const testSource = useTestSource(id); const updateSource = useUpdateSource(id); const refreshEnrichmentSection = useRefreshEnrichmentSection(id); const { data: runs, isLoading: runsLoading } = useCompanyRuns(id); const runNow = useRunCompanyNow(id); const { data: reports, isLoading: reportsLoading } = useCompanyReports(id); const generateReport = useGenerateReport(id); const { data: snapshots, isLoading: snapshotsLoading } = useSnapshots(id); const { data: alerts, isLoading: alertsLoading } = useAlerts({ company_id: id }); const markAlertRead = useMarkAlertRead(); const resolveAlert = useResolveAlert(); const { data: notificationDestinations } = useNotificationDestinations(); const { data: systemStatus } = useSystemStatus(); const [tab, setTab] = useState(initialTab); const [confirmingDelete, setConfirmingDelete] = useState(false); const [newSourceName, setNewSourceName] = useState(""); const [newSourceUrl, setNewSourceUrl] = useState(""); const [testResults, setTestResults] = useState>({}); const [expandedSnapshots, setExpandedSnapshots] = useState>(new Set()); const [frequencyOverride, setFrequencyOverride] = useState(null); const [severityOverride, setSeverityOverride] = useState(null); if (isLoading) { return

Loading…

; } if (!company) { return

Company not found.

; } const config = company.monitor_configuration; // "Running" covers the whole onboarding/run chain, not just the // monitoring-run row itself: enrichment is a separate fire-and-forget // task fired at company creation, and its own tab shouldn't look done // (nor should Pause/Delete be usable) while it's still in flight either. const isRunning = (runs?.some((r) => r.status === "queued" || r.status === "running") ?? false) || company.enrichment?.status === "pending"; const returnTo = `/companies/${id}?tab=${encodeURIComponent(tab)}`; const monitoredByName = new Map( (allCompanies ?? []) .filter((c) => c.id !== company.id) .map((c) => [c.name.trim().toLowerCase(), c.id] as const), ); const existingCompetitorKeys = new Set(company.competitors.map((c) => c.trim().toLowerCase())); const enrichmentCompetitors = (company.enrichment?.data.competitors as { name?: string }[] | undefined) ?? []; const suggestedCompetitors = [ ...new Set( enrichmentCompetitors .map((c) => companyNameFromUrl((c.name ?? "").trim())) .filter((name) => name && !existingCompetitorKeys.has(name.toLowerCase())), ), ]; const companyDestinations = (notificationDestinations ?? []).filter((d) => d.companies.some((c) => c.id === company.id), ); const emailDestination = companyDestinations.find((d) => d.type === "email"); const smsDestination = companyDestinations.find((d) => d.type === "sms"); return (
← Back to companies

{company.name}

{company.official_website && ( {company.official_website} )}
Delete this company?

{TAB_DESCRIPTIONS[tab]}

{tab === "Overview" && (

Monitoring focus

{company.monitoring_focus || "No specific focus provided — general monitoring."}

{company.description && (

Description

{company.description}

)}

Competitors

{company.competitors.length === 0 && suggestedCompetitors.length === 0 ? (

None listed.

) : (
    {company.competitors.map((c) => (
  • ))} {suggestedCompetitors.map((c) => (
  • ))}
)}
)} {tab === "Enrichment" && (
{!company.enrichment ? (
Company enrichment (NinjaPear) isn't configured for this app — no data provider key is set.
) : company.enrichment.status === "pending" ? (
Enriching this company from external data sources… this can take a few minutes.
) : ( <> {company.enrichment.status === "partial" && ( )} {company.enrichment.status === "failed" && ( )}

Company snapshot

Employees
{String(company.enrichment.data.employee_count ?? "—")}
Founded
{String(company.enrichment.data.founded_year ?? "—")}
{Array.isArray(company.enrichment.data.specialties) && (company.enrichment.data.specialties as string[]).length > 0 && ( <>

Specialties

    {(company.enrichment.data.specialties as string[]).map((s) => (
  • {s}
  • ))}
)}

Funding

{(() => { const funding = company.enrichment.data.funding as | { total_raised?: string; rounds?: Record[] } | undefined; if (!funding || (!funding.total_raised && !funding.rounds?.length)) { return (

No funding data found.

); } return ( <> {funding.total_raised && (

Total raised:{" "} {formatMoney(funding.total_raised)}

)}
    {(funding.rounds ?? []).map((r, i) => (
  • {String(r.round_name ?? "Round")} —{" "} {r.amount ? formatMoney(String(r.amount)) : "—"} {r.date ? ` (${String(r.date)})` : ""}

    {Array.isArray(r.investors) && (r.investors as string[]).length > 0 && (

    Investors: {(r.investors as string[]).join(", ")}

    )}
  • ))}
); })()}

Leadership team

{!Array.isArray(company.enrichment.data.leadership_team) || (company.enrichment.data.leadership_team as unknown[]).length === 0 ? (

No leadership team data found.

) : (
    {( company.enrichment.data.leadership_team as Record[] ).map((m, i) => (
  • {i > 0 &&
    }

    {String(m.name ?? "")}

    {m.title ? (

    {String(m.title)}

    ) : null}
    {m.work_email ? : null} {m.work_email && m.profile_url ? ( - ) : null} {m.profile_url ? ( Profile ) : null}
  • ))}
)}

Products

{!Array.isArray(company.enrichment.data.products) || (company.enrichment.data.products as unknown[]).length === 0 ? (

None found.

) : (
    {(company.enrichment.data.products as Record[]).map( (p, i) => (
  • {String(p.name ?? "")} {p.description ? ( {" "} — {String(p.description)} ) : null}
  • ), )}
)}

Recent updates

{!Array.isArray(company.enrichment.data.recent_updates) || (company.enrichment.data.recent_updates as unknown[]).length === 0 ? (

None found.

) : (
    {( company.enrichment.data.recent_updates as Record[] ).map((u, i) => (
  • {i > 0 &&
    }

    {String(u.text ?? "")}

    {u.url ? ( {String(u.url)} ) : null}
  • ))}
)}

Customers

{!Array.isArray(company.enrichment.data.customers) || (company.enrichment.data.customers as unknown[]).length === 0 ? (

None found.

) : (
    {(company.enrichment.data.customers as Record[]).map( (c, i) => { const name = String(c.name ?? ""); if (!name) return null; return (
  • ); }, )}
)}

Competitors{" "} (API-suggested)

{!Array.isArray(company.enrichment.data.competitors) || (company.enrichment.data.competitors as unknown[]).length === 0 ? (

None found.

) : (
    {( company.enrichment.data.competitors as Record[] ).map((c, i) => { const name = companyNameFromUrl(String(c.name ?? "")); if (!name) return null; return (
  • ); })}
)}
{company.enrichment.credits_spent !== null && (

{company.enrichment.credits_spent} NinjaPear Credits spent fetching this data {company.enrichment.fetched_at ? ` on ${formatDateTime(company.enrichment.fetched_at)}` : ""} .

)} )}
)} {tab === "Alerts" && (
{alertsLoading ? (

Loading…

) : !alerts || alerts.length === 0 ? (

No alerts for this company yet.

) : (
    {alerts.map((alert) => (
  • {!alert.read && ( )} {alert.resolved && ( Resolved )}
    {alert.title}

    {formatRelative(alert.created_at)} · {Math.round(alert.confidence * 100)}% confidence

    {!alert.read && ( )} {!alert.resolved && ( )}
  • ))}
)}
)} {tab === "Configuration" && config && (

Monitoring configuration

{ e.preventDefault(); const form = new FormData(e.currentTarget); updateMonitor.mutate({ frequency_type: form.get("frequency_type") as never, severity_threshold: form.get("severity_threshold") as never, timezone: String(form.get("timezone")), }); }} >
({ value: s, label: SEVERITY_LABELS[s] }))} triggerClassName="w-full px-3 py-2 text-sm" />
{updateMonitor.isSuccess && (

Configuration updated.

)}
)} {tab === "Configuration" && config && (

Notifications

Where alerts for this company get sent. Managing them here also updates the Settings page, and vice versa.

)} {tab === "Latest report" && (

{reports && reports.length > 0 ? `${reports.length} report(s) generated for this company.` : "No reports yet."}

{!runsLoading && (!runs || runs.length === 0) && (

No monitoring run has collected any evidence yet, so a report generated now will come back mostly empty by design — this app never fabricates findings without real evidence. Click "Run now" above first, then generate the report.

)} {reportsLoading ? (

Loading…

) : !reports || reports.length === 0 || !reports[0] ? (
No reports yet. Run a collection or click "Generate report now".
) : (
View latest report ({reports[0].report_type},{" "} {formatDateTime(reports[0].created_at)}) →

{reports[0].executive_summary}

{reports.length > 1 && (
    {reports.slice(1).map((r) => (
  • {r.title} — {r.report_type} {formatDateTime(r.created_at)}
  • ))}
)}
)}
)} {tab === "Sources" && (

Add a custom URL source

Website, GitHub, SEC EDGAR, and careers-page sources are found automatically the first time this company runs. Add any other public URL or RSS feed here to monitor it too.

{ e.preventDefault(); if (!newSourceName || !newSourceUrl) return; createSource.mutate( { source_type: "custom_url", name: newSourceName, base_url: newSourceUrl }, { onSuccess: () => { setNewSourceName(""); setNewSourceUrl(""); }, }, ); }} >
setNewSourceName(e.target.value)} className="focus-ring mt-1 rounded-md border border-slate-300 px-3 py-2 text-sm" placeholder="Pricing page" />
setNewSourceUrl(e.target.value)} className="focus-ring mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm" placeholder="example.com/pricing" />
{sourcesLoading ? (

Loading…

) : !sources || sources.length === 0 ? (

{runs && runs.length > 0 ? "No sources configured yet." : "Website, GitHub, and other sources are discovered automatically on the " + 'first monitoring run — click "Run now" above to get started, or add a ' + "custom URL yourself above."}

) : ( {sources.map((source) => { const isTesting = testSource.isPending && testSource.variables === source.id; const isDeleting = deleteSource.isPending && deleteSource.variables === source.id; return ( ); })}
Name Type Status Last checked Check frequency Actions
{source.name} {source.base_url && (
{source.base_url}
)}
{source.source_type} {source.status} {formatDateTime(source.last_checked)}
{testResults[source.id] && (

{testResults[source.id]}

)}
)}
)} {tab === "Monitoring history" && (
{runsLoading ? (

Loading…

) : !runs || runs.length === 0 ? (

No monitoring runs yet. Click "Run now" above to start one.

) : ( {runs.map((run) => ( ))}
Started Trigger Status Sources Items collected Notes
{formatDateTime(run.started_at ?? run.created_at)} {run.trigger_type} {run.status} {run.sources_successful}/{run.sources_attempted} succeeded {run.items_collected} {run.error_summary ?? "—"}
)}
)} {tab === "Snapshots" && (
{snapshotsLoading ? (

Loading…

) : !snapshots || snapshots.length === 0 ? (

{runs && runs.length > 0 ? "No snapshots recorded yet." : 'No snapshots yet — snapshots are captured during monitoring runs. Click "Run now" above to get started.'}

) : (
    {snapshots.map((snapshot) => { const isExpanded = expandedSnapshots.has(snapshot.id); const sourceName = sources?.find((s) => s.id === snapshot.source_id)?.name ?? "Unknown source"; return (
  • {isExpanded && (

    Hash

    {snapshot.hash}

    {snapshot.text_summary && (

    Text summary

    {snapshot.text_summary}

    )} {Object.keys(snapshot.structured_summary).length > 0 && (

    Structured summary

                                    {JSON.stringify(snapshot.structured_summary, null, 2)}
                                  
    )}
    )}
  • ); })}
)}
)}
); }