'use client' import { useEffect, useMemo, useState } from 'react' import { useQuery, useMutation } from 'convex/react' import { api } from '@/convex/_generated/api' import { useProject } from '@/components/project-context' import { Card } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' import { Separator } from '@/components/ui/separator' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { ExternalLink, Mail, Tag, Target } from 'lucide-react' type Lead = { _id: string title: string url: string snippet: string platform: string relevanceScore: number intent: string status?: string matchedKeywords: string[] matchedProblems: string[] suggestedApproach: string softPitch: boolean createdAt: number notes?: string tags?: string[] } export default function LeadsPage() { const { selectedProjectId } = useProject() const leads = useQuery( api.opportunities.listByProject, selectedProjectId ? { projectId: selectedProjectId as any, limit: 200, } : "skip" ) const updateOpportunity = useMutation(api.opportunities.updateStatus) const [selectedId, setSelectedId] = useState(null) const [notes, setNotes] = useState('') const [tags, setTags] = useState('') const [sourceFilter, setSourceFilter] = useState('all') const [ageFilter, setAgeFilter] = useState('all') const sortedLeads = useMemo(() => { if (!leads) return [] const normalized = leads as Lead[] const now = Date.now() const ageLimit = ageFilter === '24h' ? now - 24 * 60 * 60 * 1000 : ageFilter === '7d' ? now - 7 * 24 * 60 * 60 * 1000 : ageFilter === '30d' ? now - 30 * 24 * 60 * 60 * 1000 : null const filtered = normalized.filter((lead) => { if (sourceFilter !== 'all' && lead.platform !== sourceFilter) return false if (ageLimit && lead.createdAt < ageLimit) return false return true }) return [...filtered].sort((a, b) => b.createdAt - a.createdAt) }, [leads, sourceFilter, ageFilter]) const sourceOptions = useMemo(() => { if (!leads) return [] const set = new Set((leads as Lead[]).map((lead) => lead.platform)) return Array.from(set).sort((a, b) => a.localeCompare(b)) }, [leads]) const selectedLead = useMemo(() => { if (!sortedLeads.length) return null const found = sortedLeads.find((lead) => lead._id === selectedId) return found ?? sortedLeads[0] }, [sortedLeads, selectedId]) useEffect(() => { if (!selectedLead) return setNotes(selectedLead.notes || '') setTags(selectedLead.tags?.join(', ') || '') }, [selectedLead]) const handleSelect = (lead: Lead) => { setSelectedId(lead._id) setNotes(lead.notes || '') setTags(lead.tags?.join(', ') || '') } const handleSave = async () => { if (!selectedLead) return const tagList = tags .split(',') .map((tag) => tag.trim()) .filter(Boolean) await updateOpportunity({ id: selectedLead._id as any, status: selectedLead.status || 'new', notes: notes || undefined, tags: tagList.length ? tagList : undefined, }) } return (
Inbox

Newest opportunities ready for outreach.

{sortedLeads.length === 0 && ( No leads yet. Run a search to populate the inbox. )} {sortedLeads.map((lead) => ( ))}
Lead

Review the thread before reaching out.

{selectedLead && ( )}
{selectedLead ? ( <>
{Math.round(selectedLead.relevanceScore * 100)}% match {selectedLead.intent} {selectedLead.status || 'new'}
{selectedLead.title}

{selectedLead.snippet}

{selectedLead.matchedKeywords.map((keyword, index) => ( {keyword} ))}
{selectedLead.matchedProblems.map((problem, index) => ( {problem} ))}

{selectedLead.suggestedApproach}

{selectedLead.softPitch ? 'Use a softer, story-led opener.' : 'Lead with a direct solution.'}