import { useState, useEffect, useCallback } from "react";
// ─── SCORING FRAMEWORK ─────────────────────────────────────
const SCORING_CRITERIA = {
channelDifferentiation: { label: "Channel Differentiation", weight: 25, description: "Does this vendor reach audiences through channels NOT already covered by StackAdapt, Social Ads, or Google Search?" },
uniqueData: { label: "Proprietary Data / Targeting", weight: 20, description: "Does the vendor have unique audience data, intent signals, or targeting methods unavailable elsewhere?" },
attributionFit: { label: "Attribution & Measurement Fit", weight: 20, description: "Can this vendor integrate into our closed-loop attribution system (CallRail, CRM, case management)?" },
costStructure: { label: "Cost & Commitment Structure", weight: 15, description: "How favorable are the economics for a test? Low minimums, flexible terms, transparent pricing?" },
legalRelevance: { label: "Legal / PI Vertical Relevance", weight: 10, description: "Does the vendor have specific experience, case studies, or compliance for legal advertising?" },
incrementalReach: { label: "Incremental Audience Reach", weight: 10, description: "Will this vendor reach people NOT already seeing our ads through existing channels?" },
};
const CURRENT_STACK = ["Programmatic Display", "Social (Meta)", "Social (TikTok)", "Google Search", "YouTube", "Native Content", "CTV/OTT", "Digital Audio", "DOOH"];
const VERDICTS = {
strong_candidate: { label: "Strong Candidate", color: "#059669", bg: "#ecfdf5", icon: "◆" },
test_candidate: { label: "Test Candidate", color: "#d97706", bg: "#fffbeb", icon: "◇" },
low_priority: { label: "Low Priority", color: "#6b7280", bg: "#f3f4f6", icon: "○" },
pass: { label: "Pass", color: "#dc2626", bg: "#fef2f2", icon: "×" },
};
function calcScore(scores) {
let ws = 0, tw = 0;
Object.entries(scores).forEach(([k, s]) => { const w = SCORING_CRITERIA[k]?.weight || 0; ws += s * w; tw += w; });
return tw > 0 ? ((ws / tw) * 20).toFixed(1) : 0;
}
// ─── SEED DATA (Scored Vendors) ─────────────────────────────
const SEED_VENDORS = [
{
id: "expertise-forbes",
name: "Expertise / Forbes",
contactDate: "January 12, 2026",
rep: "Karen Klaczynski (Account Manager)",
callParticipants: "Andrew Fox (Foxy Digital), Brent Meredith (BDIL Marketing)",
transcriptLink: "",
uspSummary: "Pay-per-qualified-lead platform leveraging the Forbes and Expertise brand names. Operates a legal directory with SEM and social-driven traffic, pre-screens callers through a consumer concierge team (22% qualification rate), and transfers only screened leads. Month-to-month with no contract, billing in arrears. Offers Forbes Advisor badge for website credibility and claims emerging visibility in ChatGPT/AI answer results.",
channels: ["Google Search", "Social (Meta)", "Social (Other)", "Native Content"],
channelNotes: "Drives traffic to directory pages using the same SEM and social channels BDIL already runs directly. The underlying mechanism is search and social arbitrage wrapped in the Forbes brand. Karen was evasive when asked how they generate traffic ('does it really matter how they find us?').",
scores: { channelDifferentiation: 2, uniqueData: 2, attributionFit: 2, costStructure: 4, legalRelevance: 4, incrementalReach: 2 },
scoreRationale: {
channelDifferentiation: "Not a new channel. They run SEM and social ads to drive traffic to Forbes/Expertise directory pages. Same inventory BDIL already buys. Scored 2 because Forbes domain authority may capture some organic/AI-referral traffic.",
uniqueData: "No proprietary data. Geographic targeting plus basic intake screening questions. The concierge pre-screening is a service layer, not a data asset. Scored 2 for the qualification filtering value.",
attributionFit: "Claimed CallRail use but could not confirm native integration ('that would be a good question to ask on email'). Portal with 90-day CSV, claimed API but no specs. Scored 2.",
costStructure: "No contract, month-to-month, pay in arrears, $1 upfront. $900/qualified lead (1-yr window), $675/PI, $187/hit-and-run. Very low commitment risk despite premium per-unit cost. Scored 4.",
legalRelevance: "Dedicated law firm account manager. Understands PI intake, statutes, at-fault screening, UM dynamics. Works with Jacoby and Myers. Scored 4 for real PI knowledge.",
incrementalReach: "Traffic funnel is SEM and social. Both Andrew and Brent flagged 'bidding against ourselves.' Karen confirmed overlap rather than refuting it. Scored 2.",
},
keyFacts: [
"$900/call (1-yr window), $675/PI lead, $187/hit-and-run with UM",
"No contract, month-to-month, pay in arrears, $1 upfront, budget cap",
"Concierge team pre-screens: only 22% of raw calls transferred as qualified",
"41 organic calls/month in TN (raw, unqualified), no existing TN attorney",
"Claimed 30% conversion rate from qualified lead to signed client",
"Forbes Advisor badge available (now requires licensing fee, may be waived)",
"Free leads: after-hours, weekends, direct clicks, ChatGPT/AI referrals",
"Portal with lead details, Q&A, notes, dispute capability, 90-day CSV",
],
redFlags: [
"Could not provide TN traffic numbers or volume projections on the call",
"Acknowledged SEM overlap but framed as positive rather than addressing cannibalization",
"Forbes licensing now requires separate fee (was free), suggesting margin pressure",
"Needed to ask others for technical/attribution answers",
"Evasive about traffic sources ('does it really matter how they find us?')",
"No lead deduplication mechanism for BDIL's own Google Ads overlap",
],
openQuestions: [
"Actual TN traffic data across all sources?",
"Qualified lead volume projections at various budget levels?",
"Native CallRail integration specs or API docs?",
"Keyword overlap with BDIL's own Google Ads?",
"Typical traffic lift when onboarding in uncovered state?",
"Lead dedup if same consumer clicks Forbes ad AND BDIL ad?",
],
verdict: "low_priority",
verdictRationale: "Lead generation middleman in the same SEM/social channels BDIL runs directly. Forbes brand has authority, and concierge screening is real, but BDIL is building that capability through CallRail/Filevine. $900/lead is premium for traffic from inventory BDIL can buy at lower CPAs. Best revisited after BDIL's own campaigns are ramped and benchmarked.",
},
{
id: "wkrn-channel2-abc",
name: "WKRN / Channel 2 ABC (Nexstar)",
contactDate: "January 15, 2026",
rep: "Lauren (Account Executive), Sydney (Digital Sales Mgr), Carrie (Strategic Account Mgr)",
callParticipants: "Andrew Fox & Gita (Foxy Digital), Pam Meredith (BDIL Marketing Dir), Darrell (Results Media / BDIL Media Buyer)",
transcriptLink: "",
uspSummary: "Existing broadcast TV partner (ABC affiliate) owned by Nexstar, the largest media group in North America (200+ stations, acquiring Tegna). Offers linear TV roadblocking, community sponsorship integrations (Big Give Back, military heroes, etc.), owned-and-operated website inventory (wkrn.com), and an emerging podcast studio. RAM brand awareness study confirmed Blair surpassed Morgan & Morgan in awareness for the first time, attributed largely to station community sponsorships. This was an introductory alignment meeting, not a new vendor pitch.",
channels: ["Linear TV", "Owned & Operated Web (wkrn.com)", "CTV/OTT", "Podcast", "Social Cross-Promo"],
channelNotes: "Linear TV already purchased through Darrell across all Nashville stations. O&O website has pre-roll and display sponsorships already running. CTV was trialed in June but not consistent. Podcast studio just installed, early stages. The unique value is community integration events and sponsorships that no digital channel can replicate.",
scores: { channelDifferentiation: 3, uniqueData: 3, attributionFit: 2, costStructure: 3, legalRelevance: 3, incrementalReach: 4 },
scoreRationale: {
channelDifferentiation: "Linear TV is already in the BDIL mix (Darrell buys all Nashville stations). O&O website pre-roll/display overlaps with StackAdapt capabilities. However, community sponsorship integrations (Big Give Back, military heroes, live TV appearances with Blair) and the emerging podcast studio represent genuinely differentiated touchpoints. Scored 3 for the mix of overlap and unique community channels.",
uniqueData: "RAM brand awareness studies are genuinely proprietary local research that validated Blair beating Morgan & Morgan for the first time. O&O first-party audience data from wkrn.com is unique. No PI-specific intent targeting data, but local viewership insights have real strategic value. Scored 3.",
attributionFit: "No discussion of CallRail integration, API, or conversion tracking for digital placements. Andrew explicitly said digital buying stays with Foxy, keeping station in a separate reporting silo. Community sponsorships have no direct attribution path. Scored 2 because the O&O digital pieces could theoretically be pixeled but this was not discussed.",
costStructure: "Existing competitive relationship where Darrell makes all stations compete on rates. Lauren extended sponsorships through January at essentially no cost to maintain the relationship. Community sponsorship pricing not detailed. No transparent CPMs for digital. Scored 3 for the existing negotiated relationship and willingness to be flexible.",
legalRelevance: "Long history with BDIL (Lauren has worked with Darrell for 4 years, station relationship much longer). Darrell said Blair's community appearances are the #1 differentiator vs. competitors. The team understands the PI market generally but has no specialized digital PI expertise. Scored 3.",
incrementalReach: "Community sponsorship events reach audiences in a way no digital channel can replicate: live TV appearances, Big Give Back segments, military hero features, and emerging podcast. RAM study proved this drives measurable brand lift. Scored 4 because these touchpoints are genuinely incremental to programmatic/search and the brand study validates their impact.",
},
keyFacts: [
"Nexstar is largest media group in North America (200+ stations), currently acquiring Tegna",
"RAM study: Blair surpassed Morgan & Morgan in brand awareness for the first time, attributed to community sponsorships",
"Blair currently sponsors approximately 8 station initiatives per year; Andrew wants to expand to 12-16",
"Darrell roadblocks all 4 major networks at same time periods so BDIL owns every station at key day parts",
"No current programmatic digital buying through the station; just O&O sponsorships on wkrn.com",
"Podcast studio just installed; lifestyle talent beginning to record; opportunity for Blair integration",
"New creative rotating every 6 weeks; moved from 60s to 30s to 15s spots for frequency and attention span",
"Community events include Big Give Back (dogs), military hero sponsorship (Fox), and various station-specific segments",
"Andrew explicitly said Foxy is not adding any new media channels until April (data stabilization period)",
"Station has digital team of 6 strategists supporting account executives on O&O and programmatic pitches",
"CTV was trialed briefly in June but not run consistently; O&O pre-roll on wkrn.com is ongoing",
"Spanish language capability not mentioned for this station",
],
redFlags: [
"No attribution integration discussed for digital placements; station reporting is fully siloed from Foxy's dashboard",
"Andrew explicitly said he does NOT need digital media buying from the station, only ideas and community activations",
"Digital team seemed focused on pitching their O&O products rather than listening to what Foxy actually needs",
"Much of the 1.5-hour call was personal conversation and relationship building with limited tactical substance",
"Station's digital capabilities (pre-roll, display, basic CTV) directly overlap with what StackAdapt already provides",
"No pricing transparency discussed for digital products or community sponsorships during the meeting",
],
openQuestions: [
"What are the CPMs and costs for O&O digital placements on wkrn.com?",
"Can O&O placements be pixeled for Foxy's CallRail/attribution architecture?",
"What does podcast sponsorship or Blair-hosted content look like in terms of format, reach, and cost?",
"How many unique monthly visitors does wkrn.com have in the Nashville DMA?",
"Can community sponsorship performance be tracked beyond impressions (brand lift, call volume correlation)?",
"What new community sponsorship ideas can the station bring for 2026 beyond current 8 programs?",
],
verdict: "test_candidate",
verdictRationale: "WKRN is an existing and proven partner, not a new vendor. The community sponsorship integrations are the clear differentiator: the RAM study quantitatively proved that Blair's station involvement drove him past Morgan & Morgan in awareness. Expanding from 8 to 12-16 sponsorships is the highest-value play here. Digital O&O products overlap with StackAdapt and should remain low priority unless attribution can be integrated. The podcast studio is an emerging opportunity worth exploring. Recommendation: maximize community activations, deprioritize O&O digital, explore podcast as new channel.",
},
{
id: "good-karma-brands-espn",
name: "Good Karma Brands / ESPN",
contactDate: "February 2, 2026",
rep: "Connor Donlon (Digital Sales)",
callParticipants: "Andrew Fox (Foxy Digital)",
transcriptLink: "",
uspSummary: "ESPN's largest local operator through Good Karma Brands. Sells publisher-direct digital video, CTV inventory, and display ads on ESPN.com, the ESPN app, and the Fantasy app, plus the GKB Premium Sports network covering the top 20 sports publishers (NFL, NBC Sports, CBS Sports, FanDuel Sports Network, etc.). Offers guaranteed impressions with one-brand-per-page exclusivity, geo-targeting down to zip code, and Claritas as a third-party attribution partner. Presented a $50K Q2 proposal for 3.875M impressions in Nashville DMA.",
channels: ["Programmatic Display", "CTV/OTT", "In-App/Mobile"],
channelNotes: "ESPN display and video inventory is partially accessible through StackAdapt programmatically, but GKB offers publisher-direct guaranteed placements with one-brand-per-page exclusivity that programmatic remnant can't guarantee. GKB Premium Sports extends across 20+ sports publishers. The fundamental blocker is they cannot offer PMPs (Private Marketplaces), only PGs managed on their end, which prevents integration into Foxy's StackAdapt dashboard.",
scores: { channelDifferentiation: 3, uniqueData: 2, attributionFit: 2, costStructure: 3, legalRelevance: 2, incrementalReach: 3 },
scoreRationale: {
channelDifferentiation: "ESPN premium placements offer one-brand-per-page exclusivity and guaranteed live-stream mid-rolls that programmatic remnant inventory can't match. GKB Premium Sports network across 20+ publishers is a curated package. However, pre-roll and display on sports sites is fundamentally the same programmatic display/video that StackAdapt already buys. Spanish language available through GKB (not ESPN directly). Scored 3 for the premium placement quality vs. the channel overlap.",
uniqueData: "No proprietary data. Claritas is a widely available third-party attribution pixel, not unique to GKB. Geo-targeting to DMA/zip code is standard. The 'hand-curated' sport selection is a service layer. ESPN first-party audience data is accessible through programmatic buys. Scored 2.",
attributionFit: "This is the core problem. GKB cannot offer PMPs, which means inventory cannot flow into Foxy's StackAdapt dashboard and cross-channel attribution architecture. Andrew flagged this as a potential dealbreaker. Connor acknowledged the limitation and said he'd check with leadership, but historically they don't do PMPs. Claritas pixel is offered as an alternative but creates a siloed reporting environment. Scored 2.",
costStructure: "$50K for 3.875M impressions is a ~$12.90 blended CPM, which is reasonable for premium sports inventory. Fully customizable geo and sport targeting. But requires commitment to a publisher-direct buy rather than flexible programmatic allocation through StackAdapt. No performance-based pricing. Q2 flight means a 3-month commitment. Scored 3.",
legalRelevance: "Connor has no apparent legal or PI experience. He's a digital sports sales rep pitching a standard media package. No case studies with law firms, no compliance knowledge, no understanding of PI intake or case economics. Andrew had to explain that golf and Vanderbilt audiences are the wrong demographic. Scored 2.",
incrementalReach: "Sports audiences on ESPN are partially reachable through StackAdapt programmatic display and CTV. However, guaranteed premium placements (one brand per page, live stream mid-rolls) reach engaged sports viewers in higher-impact contexts than programmatic remnant. GKB network extends across 20 publishers. Spanish capability adds demographic reach. Andrew asked to remove golf (wrong PI demographic), showing targeting can be customized. Scored 3.",
},
keyFacts: [
"Good Karma Brands is ESPN's largest local operator for digital and CTV ad sales",
"Q2 proposal: $50,000 for 3,875,000 impressions (April-June), Nashville DMA targeting",
"Blended CPM approximately $12.90 across display and video",
"Display ads on ESPN.com and apps with one-brand-per-page exclusivity",
"GKB Premium Sports video network: pre-roll and mid-roll across ESPN + top 20 sports publishers (NFL, NBC Sports, CBS Sports, FanDuel)",
"100% video completion rate standard: only count fully delivered, fully watched ads",
"Geo-targeting available down to zip code level; completely customizable",
"Spanish language capability available through GKB Premium Sports (not ESPN directly)",
"Claritas offered as third-party attribution partner for pixel tracking across up to 5 pages",
"Cannot offer PMPs; only Programmatic Guarantees (PGs) managed on their end",
"Andrew offered to intro Connor to Revenue Arc (Foxy's programmatic buyer) if PMP issue is resolved",
"BDIL uses StackAdapt as their DSP; Connor was told to explore compatibility",
"Andrew explicitly said BDIL is in data stabilization through April, not adding new vendors yet",
],
redFlags: [
"Cannot offer PMPs, only publisher-managed PGs, which prevents integration into Foxy's StackAdapt dashboard and cross-channel attribution",
"Connor acknowledged this is a known issue but said historically leadership says no to PMPs",
"No PI or legal vertical experience; Connor didn't understand why golf and Vanderbilt demographics are wrong until Andrew explained",
"Claritas attribution is siloed from Foxy's CallRail/CRM architecture and does not provide closed-loop tracking to signed cases",
"ESPN premium inventory is partially available through StackAdapt already; unclear what incremental value publisher-direct adds beyond guaranteed placement",
"Andrew signaled interest in the partnership but explicitly conditioned it on PMP resolution, which may not happen",
],
openQuestions: [
"Can GKB leadership approve PMP access so inventory flows into StackAdapt?",
"If PMPs are not possible, can GKB reporting feed into Foxy's dashboard via API or data export?",
"What is the true incremental reach of publisher-direct vs. programmatic ESPN inventory through StackAdapt?",
"What does Claritas attribution actually track? Can it integrate with CallRail or Filevine?",
"Can the $50K budget be reallocated mid-flight between ESPN display and GKB video based on performance?",
"What pause-screen ad inventory is available? Andrew mentioned they already buy this programmatically.",
],
verdict: "low_priority",
verdictRationale: "Good Karma Brands offers premium ESPN and sports publisher inventory that would put BDIL in front of engaged sports audiences. However, the inability to offer PMPs is a fundamental blocker for Foxy's cross-channel attribution architecture. Without PMP access, this buy sits in a silo that cannot be optimized alongside the rest of the media stack. Connor was professional and responsive, and the $50K proposal is reasonable, but until GKB resolves the PMP limitation, the inventory is better accessed through StackAdapt's existing programmatic relationships. Revisit if GKB approves PMP access after Connor's internal discussions.",
},
{
id: "several-brands-baaha",
name: "Several Brands (Baaha / Hesh Yasser)",
contactDate: "February 3, 2026",
rep: "Hesh Yasser (Sales), Baaha (founder, not on call)",
callParticipants: "Andrew Fox (Foxy Digital)",
transcriptLink: "",
uspSummary: "Performance marketing agency specializing in MVA lead generation for 20 years (5 years focused on MVA). Owns and operates 1,800+ domains (25 in MVA, e.g., injurycompensation.com) and generates leads exclusively through Google and Bing paid search at $8M/month in ad spend. Sold 37,839 leads last year with a 20% average conversion rate across partners. Leads are form-submitted (no call center pre-screening), validated through OTP phone verification, name/email matching, and dynamic intake forms that filter for not-represented, not-at-fault, and within statute of limitations. Does not segregate high-value leads (trucking, Uber, motorcycle) from standard MVA; all delivered at the same price. Walker & Walker and Hughes & Coleman are existing clients.",
channels: ["Google Search", "Bing Search"],
channelNotes: "Direct overlap with BDIL's own Google Search campaigns run by Foxy/Gita. Andrew explicitly flagged 'competing against ourselves' and 'we're buying the same words that you are.' Hesh runs national broad match campaigns at $8M/month, which inevitably surface for Nashville queries even though they don't buy Nashville-specific terms. The difference is execution model: Several Brands captures demand through 1,800+ owned domains with optimized intake forms, while BDIL runs branded/local campaigns driving to their own site. Same pond, different fishing nets.",
scores: { channelDifferentiation: 2, uniqueData: 3, attributionFit: 4, costStructure: 4, legalRelevance: 4, incrementalReach: 3 },
scoreRationale: {
channelDifferentiation: "This is Google/Bing paid search, which is exactly what Foxy/Gita runs for BDIL. Andrew directly confronted the overlap: 'we're buying the same words that you are.' Hesh acknowledged broad match national campaigns will surface Nashville results. However, the 1,800+ owned domains represent a different capture mechanism than BDIL's branded campaigns. People searching 'injury compensation' or 'car accident lawyer' may click injurycompensation.com instead of Bart Durham's ad, representing demand BDIL's own campaigns wouldn't capture. Scored 2 for the meaningful channel overlap, bumped from 1 for the domain portfolio differentiation.",
uniqueData: "The 1,800+ owned domains are genuinely proprietary assets that no other vendor can replicate. The custom-built dashboard with partner conversion rates, accident type breakdowns (trucking, Uber, motorcycle, car), and injury severity data is unique intelligence. The OTP validation system and dynamic form logic are proprietary technology. The volume data (37,839 leads, breakdown by state and accident type) represents market intelligence that BDIL cannot get elsewhere. Scored 3.",
attributionFit: "Hesh explicitly confirmed CallRail integration capability ('30+ person engineering team, they can integrate with anything'). Leads are digital form submissions with structured data (name, phone, email, accident type, injury type, fault status), making CRM/Filevine integration straightforward. No siloed reporting concerns. Andrew did not raise any attribution objections during the call, which is notable given he flagged attribution issues with every other vendor. Scored 4.",
costStructure: "$375 per lead in Tennessee ('almost at cost' offer). At stated 20% conversion rate = $1,875 cost per signed case. No segregation of high-value leads means a trucking accident ($1M+ policy) comes at the same $375 as a standard car accident. Flexible on returns for at-fault leads. No long-term contract mentioned. Walker & Walker resells these same leads at 3x markup, which validates the wholesale pricing. Scored 4.",
legalRelevance: "Five years focused exclusively on MVA lead generation. Understands at-fault vs. not-at-fault, statute of limitations, represented vs. unrepresented, injury severity categories, and accident types (trucking, Uber, motorcycle, commercial). Hughes & Coleman was their first client and remains active. Dynamic intake forms adjust based on legal status. They chose not to launch their call center specifically because law firm partners objected to pre-screening coaching. Scored 4 for deep practical understanding of PI intake economics.",
incrementalReach: "Despite the channel overlap, Several Brands' $8M/month national Google spend and 1,800+ domains cast a much wider net than BDIL's Nashville-focused campaigns. They capture 'unbranded' searchers who would never click a Bart Durham ad but will click a generic domain like injurycompensation.com. The accident type mix shows 20-25% of leads are high-value cases (trucking, Uber, motorcycle) that may not respond to local branded campaigns. Scored 3 for genuine incremental demand capture despite operating in the same search channel.",
},
keyFacts: [
"$8 million/month Google ad spend; 20 years in lead generation, 5 years MVA-focused",
"37,839 leads sold last year with 20% average conversion rate across all partners",
"1,800+ owned domains, 25 in MVA space (e.g., injurycompensation.com)",
"Google and Bing ONLY; no social media lead generation",
"90% of leads from accidents within the last 30 days (fresh accidents)",
"No call center pre-screening; leads go directly to law firm without coaching",
"OTP phone verification, name/email matching, real-time validation filters fake leads",
"Filters: not represented, not at fault, within statute of limitations",
"Does NOT segregate high-value leads: trucking ($1M+ policy), Uber, motorcycle all delivered at same price",
"Accident type mix: ~75% car accidents, ~25% high-value (trucking, Uber, motorcycle)",
"Injury breakdown: only 1,600 'no injury' out of 38,000 leads; ~3,000 broken bones, significant catastrophic injuries",
"Lowest attrition rate claimed; highest complaint was 20% in one month from one partner",
"Tennessee pricing offered at $375/lead ('almost at cost'); ranges up to $1,200 in California",
"At 20% conversion, cost per signed case = $1,875 in Tennessee",
"Walker & Walker buys their leads and resells at 3x markup (trucking at $25K, Uber at $15K)",
"Hughes & Coleman is their first and favorite client (existing relationship)",
"30+ person engineering team; offices in Houston, engineering in Jordan, sales launching in Poland",
"Custom-built dashboard showing conversion rates by partner, accident type, injury type, geography",
"Andrew compared unfavorably to Forbes: 'I talked to Forbes the other day and I did not like their solution'",
"Andrew wants follow-up call and to bring Gita (business partner) into the conversation",
],
redFlags: [
"Direct Google Search overlap: Andrew flagged 'competing against ourselves' and Hesh acknowledged broad match will surface Nashville results",
"Leads are NOT qualified by phone; validation is form-based only (OTP, name matching). Conversion responsibility falls entirely on BDIL's intake team",
"No control over Nashville-specific volume; Hesh said 'I never know how many leads I'm going to get in Tennessee'",
"Engineering and operations are offshore (Jordan and Poland), which could affect integration support responsiveness",
"Hesh was eager to close ('I'll sell you at almost at cost') which may indicate difficulty filling Tennessee inventory or desire to lock in a new market",
"No case studies or conversion data specific to Tennessee; all stats are national aggregates",
"Andrew explicitly said BDIL is in stabilization mode and not adding vendors until the ecosystem is running correctly",
],
openQuestions: [
"What is the actual monthly lead volume available in Tennessee? Hesh couldn't specify.",
"How does broad match national spend overlap with BDIL's Nashville-specific Google campaigns? Need keyword analysis.",
"What does the integration with CallRail/Filevine actually look like? Engineering team said 'anything' but no specs provided.",
"What is the return/dispute policy for at-fault leads or leads already represented?",
"Can leads be delivered in real-time or are they batched? What is the average time from form submission to delivery?",
"What referral arrangement was Baaha discussing with Andrew separately?",
"How does the $375 TN price compare to what Hughes & Coleman pays for the same leads?",
"Can Several Brands provide deduplication against BDIL's own Google Ads clicks to avoid paying twice for the same searcher?",
],
verdict: "test_candidate",
verdictRationale: "Several Brands is the strongest lead generation vendor evaluated so far, and notably the one Andrew showed the most genuine interest in (scheduled a follow-up, wants to bring Gita in, and explicitly said Forbes was 'not good' by comparison). The $8M/month Google spend and 1,800+ domain portfolio represent a fundamentally different scale of search capture than BDIL can achieve locally. At $375/lead with 20% conversion, the $1,875 cost per case is compelling, especially since high-value trucking/Uber/motorcycle cases are not segregated. The channel overlap with BDIL's own search campaigns is a real concern, but the execution model is different enough (generic domains capturing unbranded demand) that it likely represents incremental volume rather than pure cannibalization. The CallRail integration capability addresses the attribution blocker that killed other vendors. Recommendation: after Foxy's April stabilization milestone, run a 60-day test at $5-10K/month to measure actual TN volume, conversion rate, and overlap with BDIL's own search campaigns.",
},
];
// ─── API PROMPT ─────────────────────────────────────────────
const ANALYSIS_PROMPT = `You are a media vendor analyst for a personal injury law firm (BDIL / Bart Durham Injury Law) evaluating whether new vendors offer incremental value over the current media stack: StackAdapt (programmatic display, CTV/OTT, digital audio, DOOH, native content), Social Media Ads (Meta, TikTok), and Google Search.
IMPORTANT CONTEXT: Andrew Fox of Foxy Digital is the firm's media consultant. He is extremely friendly and enthusiastic on calls, agrees with everything vendors say, and makes them feel good. His signals should NOT be read as buying signals. Focus on what the vendor actually demonstrated and committed to.
Brent Meredith is BDIL's marketing director. Any other participants are from the vendor side.
Analyze the transcript and return ONLY valid JSON (no markdown, no backticks, no preamble) with this exact structure:
{
"name": "Vendor Name",
"rep": "Sales rep name and title if mentioned",
"contactDate": "Date if mentioned, otherwise 'Unknown'",
"callParticipants": "List BDIL-side participants",
"uspSummary": "2-3 sentence objective summary of their unique selling proposition",
"channels": ["Array of channel tags from this list: Programmatic Display, CTV/OTT, Digital Audio, Social (Meta), Social (TikTok), Social (Other), Google Search, Bing/Microsoft, YouTube, DOOH, Traditional Billboards, Print/Newspaper, Radio, Direct Mail, In-App/Mobile, Native Content, Podcast, SMS/Text, Email, Streaming Video, Pharmacy/Medical Screens, Other Niche"],
"channelNotes": "Brief note on whether these channels overlap with or extend beyond the current stack",
"scores": {
"channelDifferentiation": 1-5,
"uniqueData": 1-5,
"attributionFit": 1-5,
"costStructure": 1-5,
"legalRelevance": 1-5,
"incrementalReach": 1-5
},
"scoreRationale": {
"channelDifferentiation": "Why this score, citing specific transcript evidence",
"uniqueData": "Why this score",
"attributionFit": "Why this score",
"costStructure": "Why this score",
"legalRelevance": "Why this score",
"incrementalReach": "Why this score"
},
"keyFacts": ["Array of 6-12 specific facts extracted from the transcript"],
"redFlags": ["Array of concerns or gaps identified"],
"openQuestions": ["Array of questions that need follow-up"],
"verdict": "strong_candidate OR test_candidate OR low_priority OR pass",
"verdictRationale": "2-4 sentence explanation of the verdict"
}
Score rubric:
- Channel Differentiation (25% weight): 1=full overlap with current stack, 5=entirely new channel access
- Proprietary Data (20%): 1=generic 3rd-party data, 5=exclusive data no other vendor provides
- Attribution Fit (20%): 1=black box, 5=full CallRail/CRM integration to signed case level
- Cost Structure (15%): 1=high minimum/long lock-in, 5=performance-based/no lock-in
- Legal Relevance (10%): 1=no legal experience, 5=deep PI specialist
- Incremental Reach (10%): 1=same audience pool, 5=entirely unreachable through current stack
Verdict thresholds (weighted score out of 100): Strong Candidate 75+, Test Candidate 55-74, Low Priority 35-54, Pass below 35.
Be objective and skeptical. Vendor sales reps oversell. Score based on what was actually demonstrated or confirmed, not what was promised vaguely.`;
// ─── STYLES ─────────────────────────────────────────────────
const S = {
font: "'Libre Franklin', 'DM Sans', sans-serif",
serif: "'Playfair Display', Georgia, serif",
mono: "'JetBrains Mono', 'Fira Code', monospace",
bg: "#faf9f7",
card: "#ffffff",
border: "#e8e5e0",
text: "#2d2a26",
muted: "#8a857d",
accent: "#5b7f6a",
accentLight: "#e8f0eb",
accentDark: "#3d5a4a",
warm: "#c4956a",
warmLight: "#fdf3eb",
red: "#c45d4a",
redLight: "#fdf0ed",
amber: "#b8923e",
amberLight: "#fdf8ed",
};
// ─── COMPONENTS ─────────────────────────────────────────────
function ScoreBar({ score }) {
const pct = (score / 5) * 100;
const color = score <= 2 ? S.red : score <= 3 ? S.amber : S.accent;
return (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div style={{ width: 100, height: 6, background: S.border, borderRadius: 3, overflow: "hidden" }}>
<div style={{ width: `${pct}%`, height: "100%", background: color, borderRadius: 3, transition: "width 0.5s ease" }} />
</div>
<span style={{ fontFamily: S.mono, fontSize: 12, fontWeight: 600, color }}>{score}/5</span>
</div>
);
}
function VendorCard({ vendor }) {
const [showRationale, setShowRationale] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const ws = calcScore(vendor.scores);
const v = VERDICTS[vendor.verdict];
return (
<div style={{ background: S.card, border: `1px solid ${S.border}`, borderRadius: 14, padding: "26px 30px", marginBottom: 18 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 16, flexWrap: "wrap", marginBottom: 18 }}>
<div style={{ flex: 1 }}>
<h3 style={{ fontFamily: S.serif, fontSize: 23, fontWeight: 600, margin: 0, color: S.text }}>{vendor.name}</h3>
<p style={{ fontFamily: S.font, fontSize: 12, color: S.muted, margin: "4px 0 0" }}>
{vendor.rep} · {vendor.contactDate}
</p>
<p style={{ fontFamily: S.font, fontSize: 11, color: "#b0ab a3", margin: "2px 0 0" }}>BDIL: {vendor.callParticipants}</p>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
<div style={{ textAlign: "right" }}>
<div style={{ fontFamily: S.mono, fontSize: 34, fontWeight: 700, color: S.text, lineHeight: 1 }}>{ws}</div>
<div style={{ fontSize: 9, color: S.muted, fontFamily: S.font, letterSpacing: "0.5px" }}>OUT OF 100</div>
</div>
<div style={{
fontSize: 10, fontWeight: 700, fontFamily: S.font, textTransform: "uppercase", letterSpacing: "0.8px",
color: v.color, background: v.bg, padding: "6px 14px", borderRadius: 6, whiteSpace: "nowrap",
}}>
{v.icon} {v.label}
</div>
</div>
</div>
<div style={{ background: S.accentLight, borderRadius: 8, padding: "14px 18px", marginBottom: 16, borderLeft: `3px solid ${S.accent}` }}>
<p style={{ fontFamily: S.font, fontSize: 9, color: S.accent, fontWeight: 700, textTransform: "uppercase", letterSpacing: "1px", margin: "0 0 5px" }}>USP Summary</p>
<p style={{ fontFamily: S.font, fontSize: 13, color: S.text, margin: 0, lineHeight: 1.65 }}>{vendor.uspSummary}</p>
</div>
<div style={{ marginBottom: 16 }}>
<p style={{ fontFamily: S.font, fontSize: 9, color: S.muted, fontWeight: 700, textTransform: "uppercase", letterSpacing: "1px", margin: "0 0 8px" }}>Channels</p>
<div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
{vendor.channels.map(ch => {
const overlap = CURRENT_STACK.includes(ch);
return (
<span key={ch} style={{
fontFamily: S.font, fontSize: 10, fontWeight: 600,
background: overlap ? S.warmLight : S.accentLight,
color: overlap ? S.warm : S.accentDark,
padding: "3px 10px", borderRadius: 10,
border: overlap ? `1px solid ${S.warm}33` : "none",
}}>
{ch} {overlap ? "⟳" : "✦"}
</span>
);
})}
</div>
{vendor.channelNotes && (
<p style={{ fontFamily: S.font, fontSize: 11, color: S.muted, margin: "6px 0 0", lineHeight: 1.5, fontStyle: "italic" }}>{vendor.channelNotes}</p>
)}
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px 20px", marginBottom: 16 }}>
{Object.entries(SCORING_CRITERIA).map(([k, c]) => (
<div key={k} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
<span style={{ fontFamily: S.font, fontSize: 11, color: S.muted }}>{c.label} <span style={{ color: "#ccc" }}>({c.weight}%)</span></span>
<ScoreBar score={vendor.scores[k]} />
</div>
))}
</div>
<div style={{ display: "flex", gap: 16 }}>
<button onClick={() => setShowRationale(!showRationale)} style={{ fontFamily: S.font, fontSize: 11, color: S.accent, background: "none", border: "none", cursor: "pointer", fontWeight: 600, padding: 0 }}>
{showRationale ? "Hide rationale ↑" : "Scoring rationale ↓"}
</button>
<button onClick={() => setShowDetails(!showDetails)} style={{ fontFamily: S.font, fontSize: 11, color: S.text, background: "none", border: "none", cursor: "pointer", fontWeight: 600, padding: 0 }}>
{showDetails ? "Hide details ↑" : "Facts, flags & questions ↓"}
</button>
{vendor.transcriptLink && (
<a href={vendor.transcriptLink} target="_blank" rel="noopener noreferrer" style={{ fontFamily: S.font, fontSize: 11, color: S.warm, fontWeight: 600, textDecoration: "none" }}>
View transcript ↗
</a>
)}
</div>
{showRationale && (
<div style={{ marginTop: 14, padding: "16px 18px", background: "#fafaf8", borderRadius: 8 }}>
{Object.entries(SCORING_CRITERIA).map(([k, c]) => (
<div key={k} style={{ marginBottom: 12 }}>
<p style={{ fontFamily: S.font, fontSize: 11, fontWeight: 700, color: S.text, margin: "0 0 3px" }}>{c.label}: {vendor.scores[k]}/5</p>
<p style={{ fontFamily: S.font, fontSize: 12, color: S.muted, margin: 0, lineHeight: 1.6, paddingLeft: 12, borderLeft: `2px solid ${S.border}` }}>
{vendor.scoreRationale?.[k] || "No rationale provided."}
</p>
</div>
))}
</div>
)}
{showDetails && (
<div style={{ marginTop: 14, paddingTop: 14, borderTop: `1px solid ${S.border}` }}>
{vendor.keyFacts?.length > 0 && (
<div style={{ marginBottom: 16 }}>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, color: S.text, textTransform: "uppercase", letterSpacing: "0.8px", marginBottom: 6 }}>Key Facts</p>
{vendor.keyFacts.map((f, i) => (
<p key={i} style={{ fontFamily: S.font, fontSize: 12, color: "#555", margin: "5px 0", paddingLeft: 12, borderLeft: `2px solid ${S.border}`, lineHeight: 1.55 }}>{f}</p>
))}
</div>
)}
{vendor.redFlags?.length > 0 && (
<div style={{ marginBottom: 16 }}>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, color: S.red, textTransform: "uppercase", letterSpacing: "0.8px", marginBottom: 6 }}>Red Flags</p>
{vendor.redFlags.map((f, i) => (
<p key={i} style={{ fontFamily: S.font, fontSize: 12, color: "#8b4a3a", margin: "5px 0", paddingLeft: 12, borderLeft: `2px solid ${S.red}44`, lineHeight: 1.55 }}>{f}</p>
))}
</div>
)}
{vendor.openQuestions?.length > 0 && (
<div style={{ marginBottom: 16 }}>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, color: S.amber, textTransform: "uppercase", letterSpacing: "0.8px", marginBottom: 6 }}>Open Questions</p>
{vendor.openQuestions.map((q, i) => (
<p key={i} style={{ fontFamily: S.font, fontSize: 12, color: "#7a6320", margin: "5px 0", paddingLeft: 12, borderLeft: `2px solid ${S.amber}44`, lineHeight: 1.55 }}>{q}</p>
))}
</div>
)}
<div style={{ background: VERDICTS[vendor.verdict].bg, borderRadius: 8, padding: "14px 18px" }}>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, color: VERDICTS[vendor.verdict].color, textTransform: "uppercase", letterSpacing: "0.8px", marginBottom: 5 }}>Verdict</p>
<p style={{ fontFamily: S.font, fontSize: 13, color: S.text, margin: 0, lineHeight: 1.7 }}>{vendor.verdictRationale}</p>
</div>
</div>
)}
</div>
);
}
function Matrix({ vendors }) {
if (vendors.length === 0) return <p style={{ fontFamily: S.font, fontSize: 13, color: S.muted }}>No vendors scored yet. Upload a transcript to get started.</p>;
const sorted = [...vendors].sort((a, b) => parseFloat(calcScore(b.scores)) - parseFloat(calcScore(a.scores)));
return (
<div style={{ overflowX: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse", fontFamily: S.font, fontSize: 12 }}>
<thead>
<tr style={{ borderBottom: `2px solid ${S.border}` }}>
<th style={{ textAlign: "left", padding: "10px 14px", color: S.muted, fontWeight: 700, fontSize: 10, textTransform: "uppercase", letterSpacing: "0.5px" }}>Dimension</th>
{sorted.map(v => (
<th key={v.id} style={{ textAlign: "center", padding: "10px 12px", color: S.text, fontWeight: 600, fontSize: 12, minWidth: 100 }}>{v.name}</th>
))}
</tr>
</thead>
<tbody>
{Object.entries(SCORING_CRITERIA).map(([k, c]) => (
<tr key={k} style={{ borderBottom: `1px solid #f0eeea` }}>
<td style={{ padding: "9px 14px", color: "#666" }}>{c.label} <span style={{ color: "#ccc" }}>({c.weight}%)</span></td>
{sorted.map(v => {
const s = v.scores[k];
const color = s <= 2 ? S.red : s <= 3 ? S.amber : S.accent;
return <td key={v.id} style={{ textAlign: "center", padding: "9px 12px" }}><span style={{ fontFamily: S.mono, fontWeight: 700, fontSize: 15, color }}>{s}</span></td>;
})}
</tr>
))}
<tr style={{ borderTop: `2px solid ${S.border}`, background: "#fafaf8" }}>
<td style={{ padding: "12px 14px", fontWeight: 700, color: S.text }}>Weighted Score</td>
{sorted.map(v => {
const vd = VERDICTS[v.verdict];
return (
<td key={v.id} style={{ textAlign: "center", padding: "12px" }}>
<span style={{ fontFamily: S.mono, fontWeight: 700, fontSize: 20, color: S.text }}>{calcScore(v.scores)}</span>
<div style={{ fontSize: 9, fontWeight: 700, color: vd.color, marginTop: 3, textTransform: "uppercase" }}>{vd.label}</div>
</td>
);
})}
</tr>
</tbody>
</table>
</div>
);
}
function UploadPanel({ onVendorAdded }) {
const [text, setText] = useState("");
const [status, setStatus] = useState("idle");
const [error, setError] = useState("");
const [fileName, setFileName] = useState("");
const handleFile = (e) => {
const file = e.target.files?.[0];
if (!file) return;
setFileName(file.name);
const reader = new FileReader();
reader.onload = (ev) => setText(ev.target.result);
reader.readAsText(file);
};
const analyze = async () => {
if (!text.trim()) { setError("Paste or upload a transcript first."); return; }
setStatus("analyzing");
setError("");
try {
const resp = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 4000,
system: ANALYSIS_PROMPT,
messages: [{ role: "user", content: `Analyze this vendor call transcript and return the JSON scorecard:\n\n${text.substring(0, 80000)}` }],
}),
});
const data = await resp.json();
const raw = data.content?.map(b => b.text || "").join("") || "";
const cleaned = raw.replace(/```json|```/g, "").trim();
const parsed = JSON.parse(cleaned);
parsed.id = parsed.name.toLowerCase().replace(/[^a-z0-9]/g, "-") + "-" + Date.now();
onVendorAdded(parsed);
setText("");
setFileName("");
setStatus("done");
setTimeout(() => setStatus("idle"), 3000);
} catch (err) {
console.error(err);
setError("Analysis failed. Check the transcript and try again. Error: " + err.message);
setStatus("idle");
}
};
return (
<div>
<div style={{ background: S.card, border: `2px dashed ${S.border}`, borderRadius: 14, padding: "32px 28px", textAlign: "center", marginBottom: 20 }}>
<div style={{ fontSize: 36, marginBottom: 10 }}>📄</div>
<p style={{ fontFamily: S.serif, fontSize: 18, color: S.text, margin: "0 0 6px" }}>Drop a transcript here</p>
<p style={{ fontFamily: S.font, fontSize: 12, color: S.muted, margin: "0 0 16px" }}>Upload a .txt or .pdf transcript file, or paste the text below</p>
<label style={{
fontFamily: S.font, fontSize: 12, fontWeight: 600, color: S.card, background: S.accent,
padding: "10px 24px", borderRadius: 8, cursor: "pointer", display: "inline-block",
}}>
Choose File
<input type="file" accept=".txt,.pdf,.doc,.docx" onChange={handleFile} style={{ display: "none" }} />
</label>
{fileName && <p style={{ fontFamily: S.mono, fontSize: 11, color: S.accent, marginTop: 10 }}>{fileName}</p>}
</div>
<textarea
value={text}
onChange={e => setText(e.target.value)}
placeholder="Or paste the full transcript text here..."
style={{
width: "100%", minHeight: 200, fontFamily: S.mono, fontSize: 11, lineHeight: 1.6,
padding: 18, border: `1px solid ${S.border}`, borderRadius: 10, background: "#fefefe",
resize: "vertical", color: S.text, outline: "none",
}}
/>
<div style={{ display: "flex", alignItems: "center", gap: 14, marginTop: 16 }}>
<button
onClick={analyze}
disabled={status === "analyzing" || !text.trim()}
style={{
fontFamily: S.font, fontSize: 13, fontWeight: 700, color: "#fff",
background: status === "analyzing" ? S.muted : S.accent,
border: "none", borderRadius: 8, padding: "12px 28px", cursor: status === "analyzing" ? "default" : "pointer",
transition: "background 0.2s",
}}
>
{status === "analyzing" ? "Analyzing transcript..." : status === "done" ? "✓ Vendor added!" : "Analyze & Score"}
</button>
{status === "analyzing" && (
<span style={{ fontFamily: S.font, fontSize: 12, color: S.muted }}>This takes 15-30 seconds...</span>
)}
</div>
{error && <p style={{ fontFamily: S.font, fontSize: 12, color: S.red, marginTop: 10 }}>{error}</p>}
<div style={{ marginTop: 28, padding: "18px 22px", background: S.warmLight, borderRadius: 10, border: `1px solid ${S.warm}22` }}>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, color: S.warm, textTransform: "uppercase", letterSpacing: "1px", margin: "0 0 6px" }}>How it works</p>
<p style={{ fontFamily: S.font, fontSize: 12, color: "#7a6340", margin: 0, lineHeight: 1.7 }}>
Upload or paste a call transcript. The AI reads the full conversation, extracts facts and pricing,
scores the vendor across 6 dimensions (channel differentiation, proprietary data, attribution fit,
cost structure, legal relevance, incremental reach), identifies red flags, and generates a verdict.
Andrew's enthusiasm is factored out. Results are saved and persist across sessions.
</p>
</div>
</div>
);
}
function SettingsPanel({ driveLink, setDriveLink }) {
return (
<div>
<div style={{ marginBottom: 28 }}>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, color: S.text, textTransform: "uppercase", letterSpacing: "1px", margin: "0 0 10px" }}>
Google Drive Transcript Folder
</p>
<p style={{ fontFamily: S.font, fontSize: 12, color: S.muted, margin: "0 0 12px", lineHeight: 1.6 }}>
Link to your shared Google Drive folder where call transcripts are stored. Each vendor card will show a link to this folder.
</p>
<input
type="text"
value={driveLink}
onChange={e => setDriveLink(e.target.value)}
placeholder="https://drive.google.com/drive/folders/..."
style={{
width: "100%", fontFamily: S.mono, fontSize: 12, padding: "10px 14px",
border: `1px solid ${S.border}`, borderRadius: 8, background: "#fefefe",
color: S.text, outline: "none",
}}
/>
{driveLink && (
<a href={driveLink} target="_blank" rel="noopener noreferrer" style={{ fontFamily: S.font, fontSize: 11, color: S.accent, display: "inline-block", marginTop: 8, fontWeight: 600, textDecoration: "none" }}>
Open folder ↗
</a>
)}
</div>
</div>
);
}
// ─── MAIN APP ───────────────────────────────────────────────
export default function App() {
const [vendors, setVendors] = useState([]);
const [activeTab, setActiveTab] = useState("overview");
const [driveLink, setDriveLink] = useState("");
const [loaded, setLoaded] = useState(false);
// Load from storage
useEffect(() => {
(async () => {
try {
const vResult = await window.storage.get("bdil-vendors");
const dResult = await window.storage.get("bdil-drive-link");
if (vResult?.value) {
const stored = JSON.parse(vResult.value);
// Merge: ensure all seed vendors are present, preserve any user-uploaded ones
const seedIds = new Set(SEED_VENDORS.map(v => v.id));
const userAdded = stored.filter(v => !seedIds.has(v.id));
setVendors([...SEED_VENDORS, ...userAdded]);
} else {
setVendors([...SEED_VENDORS]);
}
if (dResult?.value) setDriveLink(dResult.value);
} catch {
setVendors([...SEED_VENDORS]);
}
setLoaded(true);
})();
}, []);
// Save vendors
useEffect(() => {
if (!loaded) return;
(async () => {
try { await window.storage.set("bdil-vendors", JSON.stringify(vendors)); } catch (e) { console.error(e); }
})();
}, [vendors, loaded]);
// Save drive link
useEffect(() => {
if (!loaded) return;
(async () => {
try { await window.storage.set("bdil-drive-link", driveLink); } catch (e) { console.error(e); }
})();
}, [driveLink, loaded]);
const addVendor = useCallback((v) => {
setVendors(prev => [...prev, v]);
setActiveTab("overview");
}, []);
if (!loaded) return <div style={{ minHeight: "100vh", background: S.bg, display: "flex", alignItems: "center", justifyContent: "center" }}><p style={{ fontFamily: S.font, color: S.muted }}>Loading...</p></div>;
const tabs = [
{ id: "overview", label: "Overview" },
...vendors.map(v => ({ id: v.id, label: v.name.length > 18 ? v.name.substring(0, 18) + "..." : v.name })),
{ id: "upload", label: "+ Upload" },
{ id: "settings", label: "⚙" },
];
return (
<div style={{ minHeight: "100vh", background: S.bg, fontFamily: S.font, padding: "28px 20px" }}>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Libre+Franklin:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
<div style={{ maxWidth: 960, margin: "0 auto" }}>
{/* Header */}
<div style={{ marginBottom: 28, display: "flex", justifyContent: "space-between", alignItems: "flex-end", flexWrap: "wrap", gap: 12 }}>
<div>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "2.5px", color: S.accent, margin: "0 0 6px" }}>
BDIL Vendor Evaluation
</p>
<h1 style={{ fontFamily: S.serif, fontSize: 32, fontWeight: 600, color: S.text, margin: 0, lineHeight: 1.15 }}>
Media Vendor Scorecard
</h1>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<div style={{ textAlign: "center" }}>
<div style={{ fontFamily: S.mono, fontSize: 26, fontWeight: 700, color: S.accent, lineHeight: 1 }}>{vendors.length}</div>
<div style={{ fontFamily: S.font, fontSize: 9, color: S.muted, textTransform: "uppercase", letterSpacing: "0.5px" }}>vendors scored</div>
</div>
{driveLink && (
<a href={driveLink} target="_blank" rel="noopener noreferrer" style={{
fontFamily: S.font, fontSize: 10, fontWeight: 700, color: S.warm,
background: S.warmLight, padding: "8px 14px", borderRadius: 8, textDecoration: "none",
border: `1px solid ${S.warm}22`,
}}>
📁 Transcripts Folder
</a>
)}
</div>
</div>
{/* Tabs */}
<div style={{ display: "flex", gap: 0, marginBottom: 22, borderBottom: `2px solid ${S.border}`, overflowX: "auto", paddingBottom: 0 }}>
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)} style={{
fontFamily: S.font, fontSize: 12, fontWeight: activeTab === tab.id ? 700 : 400,
color: activeTab === tab.id ? S.accent : S.muted,
background: "none", border: "none",
borderBottom: activeTab === tab.id ? `2px solid ${S.accent}` : "2px solid transparent",
padding: "10px 16px", cursor: "pointer", marginBottom: "-2px", transition: "all 0.2s",
whiteSpace: "nowrap",
}}>
{tab.label}
</button>
))}
</div>
{/* Content */}
<div style={{
background: activeTab === "overview" ? "transparent" : S.card,
borderRadius: activeTab === "overview" ? 0 : 14,
padding: activeTab === "overview" ? 0 : "26px 30px",
border: activeTab === "overview" ? "none" : `1px solid ${S.border}`,
}}>
{activeTab === "overview" && (
<div>
<div style={{ background: S.card, border: `1px solid ${S.border}`, borderRadius: 14, padding: "24px 28px", marginBottom: 22 }}>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, color: S.text, textTransform: "uppercase", letterSpacing: "1px", margin: "0 0 14px" }}>
Comparison Matrix
</p>
<Matrix vendors={vendors} />
</div>
<p style={{ fontFamily: S.font, fontSize: 10, fontWeight: 700, color: S.text, textTransform: "uppercase", letterSpacing: "1px", margin: "0 0 14px" }}>
All Vendor Scorecards
</p>
{vendors.map(v => <VendorCard key={v.id} vendor={v} />)}
</div>
)}
{activeTab === "upload" && <UploadPanel onVendorAdded={addVendor} />}
{activeTab === "settings" && <SettingsPanel driveLink={driveLink} setDriveLink={setDriveLink} />}
{vendors.map(v => activeTab === v.id && <VendorCard key={v.id} vendor={v} />)}
</div>
<p style={{ fontFamily: S.font, fontSize: 10, color: "#c4c0b8", textAlign: "center", marginTop: 22 }}>
BDIL Media Vendor Evaluation · {vendors.length}/12 Vendors Scored
</p>
</div>
</div>
);
}