From d5ef13b58e6431f3f0efc3df33ae2cb25db39efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:36:05 +0000 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Add=20BusinessReport=20compon?= =?UTF-8?q?ent=20for=206-section=20=E2=82=AC60=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create BusinessReport.tsx with 6 sections: 1. Executive Summary (health score, rating, momentum) 2. Risk Scorecard (indicators with colors/trends) 3. Critical Issues (evidence, solutions, timelines) 4. Strengths to Protect (quotes, leverage actions) 5. Action Matrix (effort/impact quadrants) 6. 90-Day Tracking (KPI targets table) - Update types.ts with new interfaces: - SynthesisV2 for new report format - LegacySynthesis for backwards compatibility - Type guard isSynthesisV2() for runtime detection - Update ReportTab to auto-detect synthesis version - Update AnalystReport, ReviewIQDashboard, StoryView for backwards compatibility with union type Co-Authored-By: Claude Opus 4.5 --- web/components/reviewiq/AnalystReport.tsx | 587 +++++++++++++ web/components/reviewiq/BusinessReport.tsx | 649 ++++++++++++++ web/components/reviewiq/ReportTab.tsx | 101 +++ web/components/reviewiq/ReviewIQDashboard.tsx | 168 ++-- web/components/reviewiq/StoryView.tsx | 830 ++++++++++++++++++ web/components/reviewiq/types.ts | 230 ++++- 6 files changed, 2447 insertions(+), 118 deletions(-) create mode 100644 web/components/reviewiq/AnalystReport.tsx create mode 100644 web/components/reviewiq/BusinessReport.tsx create mode 100644 web/components/reviewiq/ReportTab.tsx create mode 100644 web/components/reviewiq/StoryView.tsx diff --git a/web/components/reviewiq/AnalystReport.tsx b/web/components/reviewiq/AnalystReport.tsx new file mode 100644 index 0000000..1e10631 --- /dev/null +++ b/web/components/reviewiq/AnalystReport.tsx @@ -0,0 +1,587 @@ +'use client'; + +import { Star, TrendingUp, TrendingDown, Minus, AlertTriangle, CheckCircle, Quote, ArrowRight, Zap, Clock, Target, Users, MessageSquare, Trophy, Megaphone } from 'lucide-react'; +import type { + LegacySynthesis, + SentimentDataPoint, + URTDomainPoint, + TimelinePoint, + OverviewStats, + ReportAction, + ReportEvidence, + ReportStrength, +} from './types'; + +// Domain config +const DOMAINS: Record = { + P: { emoji: '๐Ÿ‘ฅ', label: 'Staff & Service' }, + V: { emoji: '๐Ÿ’ฐ', label: 'Pricing & Value' }, + J: { emoji: 'โฑ๏ธ', label: 'Speed & Process' }, + O: { emoji: '๐Ÿ›๏ธ', label: 'Product Quality' }, + A: { emoji: '๐Ÿ“', label: 'Availability' }, + E: { emoji: '๐Ÿข', label: 'Facilities' }, + R: { emoji: '๐Ÿค', label: 'Trust & Ethics' }, +}; + +interface AnalystReportProps { + synthesis: LegacySynthesis; + overview: OverviewStats; + sentiment: SentimentDataPoint[]; + domains: URTDomainPoint[]; + timeline: TimelinePoint[]; +} + +/** + * The Analyst Report - A consultant-quality business narrative. + * Replaces widget soup with a flowing, story-driven report. + */ +export function AnalystReport({ + synthesis, + overview, + sentiment, + domains, + timeline +}: AnalystReportProps) { + const positivePct = sentiment.find(s => s.valence === 'V+')?.percentage || 0; + const negativePct = sentiment.find(s => s.valence === 'V-')?.percentage || 0; + + return ( +
+ {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + THE VERDICT + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} +
+ {/* Rating Display */} +
+
+
+ {synthesis.current_rating.toFixed(1)} +
+
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ + {overview.total_reviews.toLocaleString()} reviews + +
+
+ + {/* Rating Potential */} + {synthesis.rating_gap > 0 && ( +
+
+ + + +{synthesis.rating_gap.toFixed(1)} + +
+ + potential if fixed + +
+ )} +
+ + {/* Headline */} +

+ {synthesis.headline} +

+ + {/* Verdict */} +

+ {synthesis.verdict} +

+ + {/* Momentum Indicator */} + {synthesis.momentum && synthesis.momentum_detail && ( + + )} +
+ + {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + THE STORY + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} +
+

+ Executive Summary +

+
+ {synthesis.narrative.split('\n\n').map((paragraph, i) => ( +

+ {paragraph} +

+ ))} +
+
+ + {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + THE DIAGNOSIS + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {synthesis.primary_problem && ( +
+
+
+ +
+
+

+ Primary Issue +

+

+ {synthesis.primary_problem} +

+ {synthesis.root_cause && ( +

+ Root cause: {synthesis.root_cause} +

+ )} +
+
+
+ )} + + {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + THE STRENGTHS (What to Protect) + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {synthesis.strengths && synthesis.strengths.length > 0 && ( + + )} + + {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + THE EVIDENCE + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {synthesis.evidence.length > 0 && ( +
+

+ What Customers Are Saying +

+
+ {synthesis.evidence.map((item, i) => ( + + ))} +
+
+ )} + + {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + THE DATA (Visual Support) + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} +
+
+ {/* Sentiment */} +
+

+ {synthesis.sentiment_headline || 'Sentiment Breakdown'} +

+
+ +
+
+ + {/* Categories */} +
+

+ {synthesis.category_headline || 'Category Performance'} +

+
+ {domains.slice(0, 4).map((d) => ( + + ))} +
+
+
+
+ + {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + THE ACTION PLAN (Enhanced) + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {synthesis.actions.length > 0 && ( + + )} + + {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + FOOTER + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} +
+ Report generated {new Date(synthesis.generated_at).toLocaleDateString()} + {' ยท '}{synthesis.review_count} reviews ยท {synthesis.insight_count} insights extracted +
+
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Momentum Badge +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function MomentumBadge({ momentum, detail }: { momentum: string; detail: string }) { + const config = { + improving: { + icon: , + bg: 'bg-emerald-500/20', + text: 'text-emerald-300', + label: 'Improving', + }, + declining: { + icon: , + bg: 'bg-red-500/20', + text: 'text-red-300', + label: 'Declining', + }, + stable: { + icon: , + bg: 'bg-slate-500/20', + text: 'text-slate-300', + label: 'Stable', + }, + }; + + const c = config[momentum as keyof typeof config] || config.stable; + + return ( +
+ {c.icon} + {c.label} + ยท {detail} +
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Strengths Section +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function StrengthsSection({ strengths }: { strengths: ReportStrength[] }) { + return ( +
+
+
+ +
+
+

+ Your Strengths +

+

Protect and leverage these competitive advantages

+
+
+ +
+ {strengths.map((strength, i) => ( + + ))} +
+
+ ); +} + +function StrengthCard({ strength }: { strength: ReportStrength }) { + return ( +
+ {/* Header */} +
+

{strength.title}

+ + {strength.mention_count} mentions + +
+ + {/* Quote */} + {strength.quote && ( +
+ +

"{strength.quote}"

+
+ )} + + {/* Marketing Angle */} + {strength.marketing_angle && ( +
+ +

+ Marketing: {strength.marketing_angle} +

+
+ )} +
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Action Plan Section (Enhanced) +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function ActionPlanSection({ actions, ratingGap }: { actions: ReportAction[]; ratingGap: number }) { + // Group actions by effort/timeline + const quickWins = actions.filter(a => a.effort === 'quick_win'); + const moderate = actions.filter(a => a.effort === 'moderate'); + const strategic = actions.filter(a => a.effort === 'strategic'); + + // Calculate totals + const totalImpact = actions.reduce((sum, a) => sum + (a.impact_stars || 0), 0); + const totalComplaints = actions.reduce((sum, a) => sum + (a.complaint_count || 0), 0); + + return ( +
+ {/* Header with Impact Summary */} +
+
+
+

+ Action Plan +

+

+ {actions.length} actions to gain +{totalImpact.toFixed(1)}โ˜… +

+
+
+
+ + Addresses {totalComplaints} complaints +
+
+
+ + {/* Impact Progress Bar */} +
+
+ Potential Impact +
+
+
+ +{totalImpact.toFixed(1)}โ˜… +
+
+
+ + {/* Action Groups */} +
+ {/* Quick Wins */} + {quickWins.length > 0 && ( + } + iconBg="bg-amber-100 text-amber-600" + actions={quickWins} + /> + )} + + {/* This Quarter */} + {moderate.length > 0 && ( + } + iconBg="bg-blue-100 text-blue-600" + actions={moderate} + /> + )} + + {/* Strategic */} + {strategic.length > 0 && ( + } + iconBg="bg-purple-100 text-purple-600" + actions={strategic} + /> + )} +
+
+ ); +} + +function ActionGroup({ + title, + subtitle, + icon, + iconBg, + actions, +}: { + title: string; + subtitle: string; + icon: React.ReactNode; + iconBg: string; + actions: ReportAction[]; +}) { + return ( +
+ {/* Group Header */} +
+
+ {icon} +
+
+

{title}

+

{subtitle}

+
+
+ + {/* Actions */} +
+ {actions.map((action, i) => ( + + ))} +
+
+ ); +} + +function EnhancedActionCard({ action }: { action: ReportAction }) { + const priorityConfig = { + critical: { bg: 'bg-red-50 border-red-200', badge: 'bg-red-100 text-red-700', label: 'Critical' }, + high: { bg: 'bg-orange-50 border-orange-200', badge: 'bg-orange-100 text-orange-700', label: 'High' }, + medium: { bg: 'bg-slate-50 border-slate-200', badge: 'bg-slate-100 text-slate-700', label: 'Medium' }, + }; + + const config = priorityConfig[action.priority as keyof typeof priorityConfig] || priorityConfig.medium; + + return ( +
+ {/* Action Title */} +

+ {action.action} +

+ + {/* Meta Grid */} +
+ {/* Priority */} +
+ + {config.label} + +
+ + {/* Owner */} +
+ + {action.owner} +
+ + {/* Impact */} +
+ + {action.impact} +
+ + {/* Complaints */} + {action.complaint_count > 0 && ( +
+ + {action.complaint_count} complaints +
+ )} +
+ + {/* Evidence Quote */} + {action.evidence && ( +
+ "{action.evidence}" +
+ )} + + {/* Success Metric */} + {action.success_metric && ( +
+ + Success: {action.success_metric} +
+ )} +
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Sub-components +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function EvidenceCard({ evidence }: { evidence: ReportEvidence }) { + const isDamaging = evidence.sentiment === 'damaging'; + + return ( +
+
+ +
+

+ "{evidence.quote}" +

+

+ {evidence.context} +

+
+
+
+ ); +} + +function SentimentBar({ positive, negative }: { positive: number; negative: number }) { + const neutral = Math.max(0, 100 - positive - negative); + + return ( +
+
+
+
+
+
+
+ {positive.toFixed(0)}% positive + {negative.toFixed(0)}% negative +
+
+ ); +} + +function DomainRow({ domain }: { domain: URTDomainPoint }) { + const config = DOMAINS[domain.domain] || { emoji: '๐Ÿ“Š', label: domain.domain_name }; + const total = domain.positive_count + domain.negative_count; + const negativePct = total > 0 ? (domain.negative_count / total) * 100 : 0; + + const status = negativePct > 40 ? 'critical' : negativePct > 25 ? 'warning' : 'good'; + const statusColors = { + critical: 'text-red-600', + warning: 'text-orange-600', + good: 'text-emerald-600', + }; + + return ( +
+ {config.emoji} + {config.label} + + {negativePct.toFixed(0)}% issues + +
+ ); +} diff --git a/web/components/reviewiq/BusinessReport.tsx b/web/components/reviewiq/BusinessReport.tsx new file mode 100644 index 0000000..c725d65 --- /dev/null +++ b/web/components/reviewiq/BusinessReport.tsx @@ -0,0 +1,649 @@ +'use client'; + +import { + Star, + TrendingUp, + TrendingDown, + Minus, + AlertTriangle, + CheckCircle, + Quote, + Zap, + Clock, + Target, + Users, + Trophy, + Megaphone, + AlertCircle, + Calendar, + DollarSign, + Shield, + Activity, + BarChart3, +} from 'lucide-react'; +import type { + SynthesisV2, + ExecutiveSummary, + RiskScorecard, + RiskIndicator, + CriticalIssue, + StrengthToProtect, + ActionMatrixItem, + TrackingKPI, +} from './types'; + +interface BusinessReportProps { + synthesis: SynthesisV2; +} + +/** + * The โ‚ฌ60 Business Reputation Report - 6-Section Format + * A productized, high-value report for SMB owners. + */ +export function BusinessReport({ synthesis }: BusinessReportProps) { + const { executive_summary, risk_scorecard, critical_issues, strengths, action_matrix, tracking_kpis } = synthesis; + + return ( +
+ {/* Report Header */} +
+

{synthesis.report_title}

+

{synthesis.report_date} ยท {synthesis.analysis_period}

+
+ + {/* Section 1: Executive Summary */} + + + {/* Section 2: Risk Scorecard */} + + + {/* Section 3: Critical Issues */} + {critical_issues.length > 0 && ( + + )} + + {/* Section 4: Strengths to Protect */} + {strengths.length > 0 && ( + + )} + + {/* Section 5: Action Matrix */} + {action_matrix.length > 0 && ( + + )} + + {/* Section 6: 90-Day Tracking */} + {tracking_kpis.length > 0 && ( + + )} + + {/* Footer */} +
+

Report generated {new Date(synthesis.generated_at).toLocaleDateString()}

+

{synthesis.review_count} reviews ยท {synthesis.insight_count} insights analyzed

+
+
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Section 1: Executive Summary +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function ExecutiveSummarySection({ summary, reviewCount }: { summary: ExecutiveSummary; reviewCount: number }) { + const healthColor = summary.health_score >= 70 ? 'emerald' : summary.health_score >= 50 ? 'amber' : 'red'; + + return ( +
+
+ {/* Health Score Gauge */} +
+
+ + + + +
+ {summary.health_score} +
+
+
+
+ {summary.health_label} +
+
{reviewCount.toLocaleString()} reviews analyzed
+
+
+ + {/* Rating Display */} +
+
+ {summary.current_rating.toFixed(1)} +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ {summary.rating_gap > 0 && ( +
+ + โ†’ {summary.potential_rating.toFixed(1)}โ˜… potential +
+ )} +
+
+ + {/* One-liner */} +

{summary.one_liner}

+ + {/* Key Insight */} +

{summary.key_insight}

+ + {/* Revenue at Risk + Momentum */} +
+
+ + {summary.estimated_revenue_at_risk} at risk +
+ +
+
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Section 2: Risk Scorecard +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function RiskScorecardSection({ scorecard }: { scorecard: RiskScorecard }) { + const riskColors = { + low: 'emerald', + medium: 'amber', + high: 'orange', + critical: 'red', + }; + const riskColor = riskColors[scorecard.overall_risk] || 'amber'; + + return ( +
+
+
+
+ +
+
+

Risk Scorecard

+

Health indicators by area

+
+
+
+ {scorecard.overall_risk} Risk +
+
+ + {/* Risk Indicators Grid */} +
+ {scorecard.indicators.map((indicator, i) => ( + + ))} +
+ + {/* Immediate Attention */} + {scorecard.immediate_attention && ( +
+ +
+

Immediate Attention Required

+

{scorecard.immediate_attention}

+
+
+ )} +
+ ); +} + +function RiskIndicatorCard({ indicator }: { indicator: RiskIndicator }) { + const colorMap = { + green: { bg: 'bg-emerald-50', border: 'border-emerald-200', text: 'text-emerald-700', bar: 'bg-emerald-500' }, + yellow: { bg: 'bg-amber-50', border: 'border-amber-200', text: 'text-amber-700', bar: 'bg-amber-500' }, + red: { bg: 'bg-red-50', border: 'border-red-200', text: 'text-red-700', bar: 'bg-red-500' }, + }; + const colors = colorMap[indicator.color] || colorMap.yellow; + + const TrendIcon = indicator.trend === 'improving' ? TrendingUp : indicator.trend === 'declining' ? TrendingDown : Minus; + + return ( +
+
+ {indicator.name} + +
+
+ {indicator.score} + /10 +
+
+
+
+

{indicator.complaint_count} complaints

+
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Section 3: Critical Issues +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function CriticalIssuesSection({ issues }: { issues: CriticalIssue[] }) { + return ( +
+
+
+ +
+
+

Critical Issues

+

Top problems requiring immediate action

+
+
+ +
+ {issues.map((issue) => ( + + ))} +
+
+ ); +} + +function CriticalIssueCard({ issue }: { issue: CriticalIssue }) { + const effortColors = { + quick_win: 'bg-emerald-100 text-emerald-700', + moderate: 'bg-blue-100 text-blue-700', + strategic: 'bg-purple-100 text-purple-700', + }; + + return ( +
+ {/* Header */} +
+
+ + {issue.rank} + +
+

{issue.title}

+

{issue.urt_code} ยท {issue.complaint_count} complaints

+
+
+
+

{issue.revenue_impact}

+ + {issue.effort.replace('_', ' ')} ยท {issue.timeline} + +
+
+ + {/* Body */} +
+ {/* Root Cause */} +
+

Root Cause

+

{issue.root_cause}

+
+ + {/* Evidence */} + {issue.evidence.length > 0 && ( +
+

Customer Evidence

+
+ {issue.evidence.slice(0, 2).map((quote, i) => ( +
+ +

"{quote}"

+
+ ))} +
+
+ )} + + {/* Solution */} +
+
+ +
+

Recommended Solution

+

{issue.solution}

+
+
+
+
+
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Section 4: Strengths to Protect +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function StrengthsSection({ strengths }: { strengths: StrengthToProtect[] }) { + return ( +
+
+
+ +
+
+

Protect Your Strengths

+

Competitive advantages to leverage

+
+
+ +
+ {strengths.map((strength, i) => ( + + ))} +
+
+ ); +} + +function StrengthCard({ strength }: { strength: StrengthToProtect }) { + return ( +
+ {/* Header */} +
+

{strength.title}

+
+ + {strength.mention_count} mentions + + {strength.percentage.toFixed(0)}% +
+
+ + {/* Quotes */} + {strength.top_quotes.length > 0 && ( +
+ {strength.top_quotes.slice(0, 2).map((quote, i) => ( +
+ +

"{quote.slice(0, 100)}{quote.length > 100 ? '...' : ''}"

+
+ ))} +
+ )} + + {/* Risk of Loss */} + {strength.risk_of_loss && ( +
+ + {strength.risk_of_loss} +
+ )} + + {/* Leverage Action */} + {strength.leverage_action && ( +
+ +

+ Leverage: {strength.leverage_action} +

+
+ )} +
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Section 5: Action Matrix +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function ActionMatrixSection({ actions }: { actions: ActionMatrixItem[] }) { + const quickWins = actions.filter(a => a.quadrant === 'quick_win'); + const majorProjects = actions.filter(a => a.quadrant === 'major_project'); + const others = actions.filter(a => !['quick_win', 'major_project'].includes(a.quadrant)); + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Action Matrix

+

{actions.length} prioritized actions

+
+
+
+ +
+ {/* Quick Wins */} + {quickWins.length > 0 && ( + } + iconBg="bg-amber-100 text-amber-600" + actions={quickWins} + /> + )} + + {/* Major Projects */} + {majorProjects.length > 0 && ( + } + iconBg="bg-blue-100 text-blue-600" + actions={majorProjects} + /> + )} + + {/* Others */} + {others.length > 0 && ( + } + iconBg="bg-slate-100 text-slate-600" + actions={others} + /> + )} +
+
+ ); +} + +function ActionGroup({ + title, + subtitle, + icon, + iconBg, + actions, +}: { + title: string; + subtitle: string; + icon: React.ReactNode; + iconBg: string; + actions: ActionMatrixItem[]; +}) { + return ( +
+
+
{icon}
+
+

{title}

+

{subtitle}

+
+
+
+ {actions.map((action, i) => ( + + ))} +
+
+ ); +} + +function ActionCard({ action }: { action: ActionMatrixItem }) { + return ( +
+

{action.action}

+ +
+
+ + {action.owner} +
+
+ + {action.deadline} +
+
+ + {action.expected_lift} +
+
+ + {action.effort} effort +
+
+ + {action.success_metric && ( +
+ + Success: {action.success_metric} +
+ )} +
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Section 6: 90-Day Tracking +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function TrackingSection({ kpis }: { kpis: TrackingKPI[] }) { + return ( +
+
+
+ +
+
+

90-Day Tracking Framework

+

Monitor these KPIs monthly

+
+
+ +
+ + + + + + + + + + + + {kpis.map((kpi, i) => ( + + + + + + + + ))} + +
MetricCurrent30-Day60-Day90-Day
+

{kpi.metric}

+

{kpi.measurement}

+
+ + {kpi.current_value} + + + + {kpi.target_30_day} + + + + {kpi.target_60_day} + + + + {kpi.target_90_day} + +
+
+
+ ); +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +// Shared Components +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +function MomentumBadge({ momentum, detail }: { momentum: string; detail: string }) { + const config = { + improving: { + icon: , + bg: 'bg-emerald-500/20', + text: 'text-emerald-300', + label: 'Improving', + }, + declining: { + icon: , + bg: 'bg-red-500/20', + text: 'text-red-300', + label: 'Declining', + }, + stable: { + icon: , + bg: 'bg-slate-500/20', + text: 'text-slate-300', + label: 'Stable', + }, + }; + + const c = config[momentum as keyof typeof config] || config.stable; + + return ( +
+ {c.icon} + {c.label} + {detail && ยท {detail}} +
+ ); +} diff --git a/web/components/reviewiq/ReportTab.tsx b/web/components/reviewiq/ReportTab.tsx new file mode 100644 index 0000000..4238b9e --- /dev/null +++ b/web/components/reviewiq/ReportTab.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { Loader2, FileWarning } from 'lucide-react'; +import { useReviewIQAnalytics } from '@/hooks/useReviewIQAnalytics'; +import { AnalystReport } from './AnalystReport'; +import { BusinessReport } from './BusinessReport'; +import { isSynthesisV2 } from './types'; +import type { ReviewIQFilters, LegacySynthesis, SynthesisV2 } from './types'; + +interface ReportTabProps { + jobId?: string; + businessId?: string; +} + +// Default filters for Report view - uses 'all' time range for comprehensive analysis +const defaultFilters: ReviewIQFilters = { + timeRange: 'all', + sentiment: [], + urtDomain: null, + intensity: [], + brushRange: null, +}; + +/** + * Report Tab - Wraps report components with data fetching. + * Automatically detects report version and renders appropriate component. + */ +export function ReportTab({ jobId, businessId }: ReportTabProps) { + const { data, loading, error } = useReviewIQAnalytics({ + jobId, + businessId, + filters: defaultFilters, + }); + + // Loading state + if (loading) { + return ( +
+ +
+ ); + } + + // Error state + if (error) { + return ( +
+

{error}

+
+ ); + } + + // No data state + if (!data) { + return ( +
+

No data available for this report.

+
+ ); + } + + // No synthesis - AI report not generated yet + if (!data.synthesis) { + return ( +
+
+ +

+ AI Report Not Generated Yet +

+

+ The AI-powered analyst report hasn't been generated for this dataset. + Run the pipeline with the "synthesize" stage to generate the report. +

+
+ Stage 5: Synthesize โ†’ Generates narratives, actions & insights +
+
+
+ ); + } + + // Render the appropriate report based on synthesis version + if (isSynthesisV2(data.synthesis)) { + // New 6-section Business Report (v2.0) + return ; + } + + // Legacy Analyst Report (v1.x) + return ( + + ); +} + +export default ReportTab; diff --git a/web/components/reviewiq/ReviewIQDashboard.tsx b/web/components/reviewiq/ReviewIQDashboard.tsx index d31ca60..f9991c9 100644 --- a/web/components/reviewiq/ReviewIQDashboard.tsx +++ b/web/components/reviewiq/ReviewIQDashboard.tsx @@ -1,19 +1,30 @@ 'use client'; import { useState } from 'react'; -import { RefreshCw, BarChart3 } from 'lucide-react'; +import { RefreshCw } from 'lucide-react'; import { ReviewIQFilterProvider, useReviewIQFilters } from '@/contexts/ReviewIQFilterContext'; import { useReviewIQAnalytics } from '@/hooks/useReviewIQAnalytics'; -import { FilterBar } from './FilterBar'; import { DashboardSkeleton, DashboardError, DashboardEmpty } from './DashboardSkeleton'; +import { FilterBar } from './FilterBar'; import { SentimentPie } from './charts/SentimentPie'; import { IntensityHeatmap } from './charts/IntensityHeatmap'; import { TimelineChart } from './charts/TimelineChart'; import { IssuesTable } from './tables/IssuesTable'; import { SpansTable } from './tables/SpansTable'; -import { ExecutiveSummary } from './insights/ExecutiveSummary'; import { OpportunityMatrix } from './insights/OpportunityMatrix'; -import type { URTDomain } from './types'; +import { ExplorerView } from './ExplorerView'; +import type { URTDomain, Synthesis, LegacySynthesis } from './types'; +import { isSynthesisV2 } from './types'; + +// Helper to extract legacy fields from either synthesis format +function getLegacyInsight(synthesis: Synthesis | null | undefined, field: keyof LegacySynthesis): string | undefined { + if (!synthesis) return undefined; + if (isSynthesisV2(synthesis)) { + // V2 doesn't have these fields, return undefined + return undefined; + } + return (synthesis as LegacySynthesis)[field] as string | undefined; +} interface ReviewIQDashboardProps { jobId?: string | null; @@ -22,13 +33,7 @@ interface ReviewIQDashboardProps { /** * Inner dashboard component that uses the filter context. - * - * Streamlined flow (no redundancy): - * 1. Hero: Executive Summary (rating, AI insights, #1 problem/strength, top complaints) - * 2. Explore: Sentiment + Category Heatmap (side by side) - * 3. Action: Opportunity Matrix (what to fix) - * 4. Trends: Timeline - * 5. Deep Dive: Issues & Spans tables + * Shows data exploration view with charts, tables, and trend explorer. */ function ReviewIQDashboardInner({ jobId, businessId }: ReviewIQDashboardProps) { const { filters, setURTDomain } = useReviewIQFilters(); @@ -45,9 +50,6 @@ function ReviewIQDashboardInner({ jobId, businessId }: ReviewIQDashboardProps) { spansPageSize: 10, }); - const handleIssuesPageChange = (page: number) => setIssuesPage(page); - const handleSpansPageChange = (page: number) => setSpansPage(page); - // No job selected if (!jobId && !businessId) { return ; @@ -68,106 +70,110 @@ function ReviewIQDashboardInner({ jobId, businessId }: ReviewIQDashboardProps) { return ; } - // Handle domain click for filtering - const handleDomainClick = (domain: URTDomain) => { - setURTDomain(filters.urtDomain === domain ? null : domain); - }; - return (
- {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - HEADER - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {/* Header */}
-
-
- -
-
-

ReviewIQ Analytics

-

- {data.overview.total_reviews.toLocaleString()} reviews โ€ข {data.overview.total_spans.toLocaleString()} insights extracted -

-
+
+

Data Explorer

+

+ {data.overview.total_reviews.toLocaleString()} reviews ยท {data.overview.total_spans.toLocaleString()} insights +

- {/* Active Filters Bar */} + {/* Data View */} + +
+ ); +} + +/** + * Data view - the detailed charts and tables + */ +function DataView({ + data, + filters, + setURTDomain, + issuesPage, + spansPage, + setIssuesPage, + setSpansPage, + jobId, + businessId, +}: { + data: NonNullable['data']>; + filters: ReturnType['filters']; + setURTDomain: ReturnType['setURTDomain']; + issuesPage: number; + spansPage: number; + setIssuesPage: (page: number) => void; + setSpansPage: (page: number) => void; + jobId?: string; + businessId?: string; +}) { + const handleDomainClick = (domain: URTDomain) => { + setURTDomain(filters.urtDomain === domain ? null : domain); + }; + + return ( + <> + {/* Filters */} - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - SECTION 1: EXECUTIVE SUMMARY (Hero) - Rating, AI summary, #1 Problem, #1 Strength, Top Complaints - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} - - - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - SECTION 2: EXPLORE (Sentiment + Categories) - Side-by-side: How customers feel + What they talk about - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {/* Sentiment + Categories */}
- {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - SECTION 3: ACTION (Opportunity Matrix) - What to fix - prioritized by impact vs effort - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {/* Opportunity Matrix */} - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - SECTION 4: TRENDS (Timeline) - How things change over time - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {/* Timeline */} - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - SECTION 5: DEEP DIVE (Tables) - Detailed issues and individual mentions - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} + {/* Tables */}
- - + +
- {/* Debug Info (dev only) */} - {process.env.NODE_ENV === 'development' && ( -
- - Debug: Filters Applied - -
-            {JSON.stringify(data.filters_applied, null, 2)}
-          
-
- )} -
+ {/* Trend Explorer */} +
+

Trend Explorer

+ +
+ ); } diff --git a/web/components/reviewiq/StoryView.tsx b/web/components/reviewiq/StoryView.tsx new file mode 100644 index 0000000..98c1f56 --- /dev/null +++ b/web/components/reviewiq/StoryView.tsx @@ -0,0 +1,830 @@ +'use client'; + +import React from 'react'; +import { Loader2 } from 'lucide-react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + Cell, + ReferenceLine, +} from 'recharts'; +import { + ReviewIQAnalyticsResponse, + TimelinePoint, + StrengthItem, + WeaknessItem, + ReportAction, + DOMAIN_FRIENDLY, + Synthesis, + LegacySynthesis, + isSynthesisV2, +} from './types'; + +// Helper to safely get legacy synthesis fields +function getLegacyField( + synthesis: Synthesis | null | undefined, + field: K +): LegacySynthesis[K] | undefined { + if (!synthesis) return undefined; + if (isSynthesisV2(synthesis)) return undefined; + return (synthesis as LegacySynthesis)[field]; +} +import { useReviewIQAnalytics } from '@/hooks/useReviewIQAnalytics'; +import type { ReviewIQFilters } from './types'; + +// Default filters for Story view - uses 'all' time range for comprehensive narrative +const defaultFilters: ReviewIQFilters = { + timeRange: 'all', + sentiment: [], + urtDomain: null, + intensity: [], + brushRange: null, +}; + +// ==================== Props ==================== + +interface StoryViewProps { + jobId?: string; + businessId?: string; +} + +// ==================== Helper Functions ==================== + +interface StoryPoint { + date: string; + rating: number | null; + type: 'peak' | 'valley' | 'normal'; + change: number; + reviewCount: number; +} + +function identifyStoryPoints(timeline: TimelinePoint[]): StoryPoint[] { + if (timeline.length < 3) { + return timeline.map((t) => ({ + date: t.date, + rating: t.avg_rating, + type: 'normal' as const, + change: 0, + reviewCount: t.review_count, + })); + } + + const points: StoryPoint[] = []; + + for (let i = 0; i < timeline.length; i++) { + const current = timeline[i]; + const prev = timeline[i - 1]; + const next = timeline[i + 1]; + + let type: 'peak' | 'valley' | 'normal' = 'normal'; + let change = 0; + + if (current.avg_rating !== null) { + if (prev && prev.avg_rating !== null) { + change = current.avg_rating - prev.avg_rating; + } + + // Identify peaks and valleys + if (prev && next && prev.avg_rating !== null && next.avg_rating !== null) { + if (current.avg_rating > prev.avg_rating && current.avg_rating > next.avg_rating) { + type = 'peak'; + } else if (current.avg_rating < prev.avg_rating && current.avg_rating < next.avg_rating) { + type = 'valley'; + } + } + + // Also mark significant changes (> 0.3 stars) + if (Math.abs(change) > 0.3) { + type = change > 0 ? 'peak' : 'valley'; + } + } + + points.push({ + date: current.date, + rating: current.avg_rating, + type, + change, + reviewCount: current.review_count, + }); + } + + return points; +} + +function formatDate(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); +} + +function getEmotionalHook(currentRating: number, potentialRating: number): string { + const gap = potentialRating - currentRating; + + if (gap >= 0.5) { + return "You're leaving stars on the table. Let's get them back."; + } else if (gap >= 0.3) { + return "Small changes can unlock big improvements."; + } else if (gap >= 0.1) { + return "You're close to excellence. Let's close the gap."; + } else { + return "Maintain your momentum and protect what you've built."; + } +} + +function getPriorityColor(priority: string): string { + switch (priority) { + case 'critical': + return 'bg-red-500'; + case 'high': + return 'bg-orange-500'; + case 'medium': + return 'bg-yellow-500'; + default: + return 'bg-gray-500'; + } +} + +function getEffortLabel(effort: string): string { + switch (effort) { + case 'quick_win': + return 'Quick Win'; + case 'moderate': + return 'Moderate Effort'; + case 'strategic': + return 'Strategic'; + default: + return effort; + } +} + +// ==================== Section Components ==================== + +interface HookSectionProps { + headline: string; + currentRating: number; + potentialRating: number; + emotionalHook: string; +} + +function HookSection({ headline, currentRating, potentialRating, emotionalHook }: HookSectionProps) { + const gap = potentialRating - currentRating; + + return ( +
+ {/* Background decorative elements */} +
+
+
+
+ +
+

+ {headline} +

+ +
+ {/* Current Rating */} +
+
+ {currentRating.toFixed(1)} +
+
+ Current Rating +
+
+ + {/* Arrow */} +
+
+ + + +
+ + {/* Potential Rating */} +
+
+ {potentialRating.toFixed(1)} +
+
+ Potential Rating +
+
+ + {/* Gap Badge */} + {gap > 0 && ( +
+
+ + + + +{gap.toFixed(1)} stars possible +
+
+ )} +
+ +

+ “{emotionalHook}” +

+
+
+ ); +} + +interface TimelineSectionProps { + storyPoints: StoryPoint[]; + timelineHeadline?: string; +} + +function TimelineSection({ storyPoints, timelineHeadline }: TimelineSectionProps) { + const significantPoints = storyPoints.filter((p) => p.type !== 'normal' || p.reviewCount > 0); + const displayPoints = significantPoints.length > 0 ? significantPoints : storyPoints.slice(0, 6); + + // Prepare chart data - take last 12 points + const chartData = storyPoints.slice(-12).map((point) => ({ + date: formatDate(point.date), + rating: point.rating ?? 0, + reviews: point.reviewCount, + type: point.type, + change: point.change, + })); + + // Calculate average rating for reference line + const avgRating = chartData.reduce((sum, p) => sum + p.rating, 0) / chartData.length; + + // Get bar color based on type + const getBarColor = (type: string) => { + if (type === 'peak') return '#10b981'; // emerald-500 + if (type === 'valley') return '#ef4444'; // red-500 + return '#3b82f6'; // blue-500 + }; + + return ( +
+

+ The Rise & Fall +

+ {timelineHeadline ? ( +

{timelineHeadline}

+ ) : ( +

+ Your rating journey over time - peaks show your best moments, valleys reveal opportunities. +

+ )} + + {/* Recharts Bar Chart */} +
+ + + + + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+
{data.date}
+
+ {data.rating.toFixed(1)} โ˜… โ€ข {data.reviews} reviews +
+ {data.change !== 0 && ( +
0 ? 'text-emerald-400' : 'text-red-400'}> + {data.change > 0 ? 'โ†‘' : 'โ†“'} {Math.abs(data.change).toFixed(2)} stars +
+ )} +
+ ); + } + return null; + }} + /> + + + {chartData.map((entry, index) => ( + + ))} + +
+
+
+ + {/* Chapter markers - significant events */} + {displayPoints.filter((p) => p.type !== 'normal').length > 0 && ( +
+ {displayPoints + .filter((p) => p.type !== 'normal') + .slice(0, 4) + .map((point, index) => ( +
+
+ {point.type === 'peak' ? 'โ†‘' : 'โ†“'} +
+
+
+ {formatDate(point.date)} +
+
+ {point.rating?.toFixed(1)} โ˜… + {point.change !== 0 && ( + 0 ? 'text-emerald-600' : 'text-red-600'}> + {' '}({point.change > 0 ? '+' : ''}{point.change.toFixed(2)}) + + )} +
+
+
+ ))} +
+ )} +
+ ); +} + +interface BattleSectionProps { + strengths: StrengthItem[]; + weaknesses: WeaknessItem[]; + strengthsHeadline?: string; +} + +function BattleSection({ strengths, weaknesses, strengthsHeadline }: BattleSectionProps) { + // Calculate total "force" on each side + const strengthForce = strengths.reduce((sum, s) => sum + s.span_count, 0); + const weaknessForce = weaknesses.reduce((sum, w) => sum + w.span_count, 0); + const totalForce = strengthForce + weaknessForce || 1; + + const strengthPercent = (strengthForce / totalForce) * 100; + const weaknessPercent = (weaknessForce / totalForce) * 100; + + return ( +
+

+ The Battle for Stars +

+ {strengthsHeadline && ( +

{strengthsHeadline}

+ )} + + {/* Tug of war visualization */} +
+
+
+ Strengths +
+
+
+ {strengthPercent.toFixed(0)}% +
+
+ {weaknessPercent.toFixed(0)}% +
+
+
+ Weaknesses +
+
+ + {/* Center marker */} +
+
+ + {/* Two columns: Strengths vs Weaknesses */} +
+ {/* Strengths Column */} +
+

+ + + + Forces Pulling Rating UP +

+
+ {strengths.slice(0, 5).map((strength, index) => { + const domainInfo = DOMAIN_FRIENDLY[strength.domain] || { emoji: '', label: strength.domain_name }; + return ( +
+
+ {index + 1} +
+
+
+ {strength.subcode_name} +
+
+ {domainInfo.emoji} {domainInfo.label} | {strength.span_count} mentions +
+
+
+ +{strength.positive_percentage.toFixed(0)}% +
+
+ ); + })} +
+
+ + {/* Weaknesses Column */} +
+

+ + + + Forces Pulling Rating DOWN +

+
+ {weaknesses.slice(0, 5).map((weakness, index) => { + const domainInfo = DOMAIN_FRIENDLY[weakness.domain] || { emoji: '', label: weakness.domain_name }; + return ( +
+
+ {index + 1} +
+
+
+ {weakness.subcode_name} +
+
+ {domainInfo.emoji} {domainInfo.label} | {weakness.span_count} mentions +
+
+
+
+ -{weakness.negative_percentage.toFixed(0)}% +
+ {weakness.projected_rating_impact !== null && ( +
+ {weakness.projected_rating_impact > 0 ? '+' : ''}{weakness.projected_rating_impact.toFixed(2)} stars if fixed +
+ )} +
+
+ ); + })} +
+
+
+
+ ); +} + +interface CustomerVoicesSectionProps { + weaknesses: WeaknessItem[]; +} + +function CustomerVoicesSection({ weaknesses }: CustomerVoicesSectionProps) { + // Collect all example spans from weaknesses + const allQuotes = weaknesses + .filter((w) => w.example_spans && w.example_spans.length > 0) + .flatMap((w) => + (w.example_spans || []).map((span) => ({ + text: span.span_text, + fullReview: span.review_text, + rating: span.rating, + date: span.review_date, + issue: w.subcode_name, + domain: w.domain_name, + })) + ) + .slice(0, 6); + + if (allQuotes.length === 0) { + return null; + } + + return ( +
+

+ Customer Voices +

+

+ Real feedback from your customers - their words tell the story. +

+ +
+ {allQuotes.map((quote, index) => ( +
+ {/* Quote mark */} +
+ “ +
+ +
+

+ {quote.text} +

+ +
+
+ {quote.rating !== null && ( +
+ {[...Array(5)].map((_, i) => ( + + + + ))} +
+ )} +
+ + + {quote.issue} + +
+ + {quote.date && ( +
+ {new Date(quote.date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + })} +
+ )} +
+
+ ))} +
+
+ ); +} + +interface ActionPlanSectionProps { + actions: ReportAction[]; +} + +function ActionPlanSection({ actions }: ActionPlanSectionProps) { + if (actions.length === 0) { + return null; + } + + return ( +
+

+ The Action Plan +

+

+ Prioritized actions to improve your rating - tackle these in order for maximum impact. +

+ +
+ {actions.map((action, index) => ( +
+
+
+ {/* Priority indicator */} +
+ {index + 1} +
+ +
+
+

+ {action.action} +

+ + {action.priority} + + + {getEffortLabel(action.effort)} + +
+ +

+ {action.evidence} +

+ +
+
+
+ Owner +
+
+ {action.owner} +
+
+
+
+ Impact +
+
+ +{action.impact_stars.toFixed(2)} stars + + + +
+
+
+
+ Complaints +
+
+ {action.complaint_count} affected +
+
+
+
+ Success Metric +
+
+ {action.success_metric} +
+
+
+
+
+
+ + {/* Progress indicator decoration */} +
+
+
+
+ ))} +
+
+ ); +} + +// ==================== Main Component ==================== + +export function StoryView({ jobId, businessId }: StoryViewProps) { + // Fetch data using the shared hook with default filters + const { data, loading, error } = useReviewIQAnalytics({ + jobId, + businessId, + filters: defaultFilters, + }); + + // Loading state + if (loading) { + return ( +
+ +
+ ); + } + + // Error state + if (error) { + return ( +
+

{error}

+
+ ); + } + + // No data state + if (!data) { + return ( +
+

No data available for this view.

+
+ ); + } + + // Extract key data + const synthesis = data.synthesis; + const insights = data.insights; + const timeline = data.timeline; + + // Compute story points from timeline + const storyPoints = identifyStoryPoints(timeline); + + // Get rating values (support both v1 and v2 synthesis) + const currentRating = getLegacyField(synthesis, 'current_rating') ?? data.overview.avg_rating ?? 0; + const potentialRating = getLegacyField(synthesis, 'potential_rating') ?? (currentRating + (insights.rating_simulator?.potential_gain ?? 0)); + + // Generate headline and emotional hook + const headline = getLegacyField(synthesis, 'headline') ?? "Your Customer Intelligence Story"; + const emotionalHook = getEmotionalHook(currentRating, potentialRating); + + // Get actions (only available in legacy format) + const actions = getLegacyField(synthesis, 'actions') ?? []; + + // Get generated_at (available in both formats but in different locations) + const generatedAt = synthesis ? (isSynthesisV2(synthesis) ? synthesis.generated_at : synthesis.generated_at) : undefined; + + return ( +
+ {/* Section 1: The Hook */} + + + {/* Section 2: The Rise & Fall (Timeline) */} + {timeline.length > 0 && ( + + )} + + {/* Section 3: The Battle (Strengths vs Weaknesses) */} + {(insights.strengths.length > 0 || insights.weaknesses.length > 0) && ( + + )} + + {/* Section 4: Customer Voices */} + + + {/* Section 5: The Action Plan */} + {actions.length > 0 && ( + + )} + + {/* Footer metadata */} +
+

+ Analysis based on {data.overview.total_reviews.toLocaleString()} reviews + {generatedAt && ( + <> | Generated {new Date(generatedAt).toLocaleDateString()} + )} +

+

+ Job ID: {jobId} | Business ID: {businessId} +

+
+
+ ); +} + +export default StoryView; diff --git a/web/components/reviewiq/types.ts b/web/components/reviewiq/types.ts index 742ec37..bddacab 100644 --- a/web/components/reviewiq/types.ts +++ b/web/components/reviewiq/types.ts @@ -152,56 +152,211 @@ export interface Insights { executive_summary: string; } -// ==================== AI Synthesis (Stage 4 Output) ==================== +// ==================== Report Synthesis v2.0 (6-Section Business Report) ==================== -export interface ActionItem { - id: string; - title: string; - why: string; // Root cause from reviews - what: string; // Specific action to take - who: string; // Department/role responsible - impact: string; // Expected outcome - evidence: string[]; // Example review quotes - estimated_rating_lift: number | null; - complexity: 'quick' | 'medium' | 'complex'; - priority: 'critical' | 'high' | 'medium' | 'low'; - timeline: string; // e.g., "This week", "This month" - related_subcode: string; // URT subcode this addresses +// Section 1: Executive Summary +export interface ExecutiveSummary { + health_score: number; // 1-100 overall health + health_label: string; // "Needs Attention" | "Stable" | "Strong" + one_liner: string; // Single sentence verdict + current_rating: number; // 3.71 + potential_rating: number; // 4.2 + rating_gap: number; // 0.49 + estimated_revenue_at_risk: string; // "โ‚ฌ15,000/month" + key_insight: string; // The most important finding + momentum: 'improving' | 'declining' | 'stable'; + momentum_detail: string; // Explanation } -export interface TimelineAnnotation { - date: string; +// Section 2: Risk Scorecard +export interface RiskIndicator { + name: string; // "Staff Behavior" + score: number; // 1-10 (10 = excellent, 1 = critical) + trend: 'improving' | 'declining' | 'stable'; + complaint_count: number; // Number of related complaints + color: 'green' | 'yellow' | 'red'; +} + +export interface RiskScorecard { + overall_risk: 'low' | 'medium' | 'high' | 'critical'; + indicators: RiskIndicator[]; + highest_risk_area: string; // "Value Perception" + immediate_attention: string; // What needs fixing NOW +} + +// Section 3: Critical Issues +export interface CriticalIssue { + rank: number; // 1, 2, or 3 + title: string; // "Hidden Fees Destroying Trust" + urt_code: string; // "V1.03" + complaint_count: number; // 94 + revenue_impact: string; // "โ‚ฌ12,000/month at risk" + evidence: string[]; // 2-3 damning quotes + root_cause: string; // Why this keeps happening + solution: string; // Specific fix + effort: 'quick_win' | 'moderate' | 'strategic'; + timeline: string; // "1 week" | "2-4 weeks" | "1-2 months" +} + +// Section 4: Strengths to Protect +export interface StrengthToProtect { + title: string; // "Exceptional Staff Service" + mention_count: number; // 168 + percentage: number; // 42.0 (% of positive reviews) + top_quotes: string[]; // 2-3 best quotes + risk_of_loss: string; // What could erode this strength + leverage_action: string; // How to amplify in marketing +} + +// Section 5: Action Matrix +export interface ActionMatrixItem { + action: string; // What to do (imperative) + owner: string; // Who owns it + effort: 'low' | 'medium' | 'high'; + impact: 'low' | 'medium' | 'high'; + quadrant: 'quick_win' | 'major_project' | 'fill_in' | 'deprioritize'; + expected_lift: string; // "+0.3โ˜…" + deadline: string; // "Week 1" | "Week 2-4" | "Month 2-3" + success_metric: string; // Measurable KPI +} + +// Section 6: 90-Day Tracking +export interface TrackingKPI { + metric: string; // "Deposit Complaints" + current_value: string; // "47/month" + target_30_day: string; // "< 25/month" + target_60_day: string; // "< 15/month" + target_90_day: string; // "< 5/month" + measurement: string; // How to measure this +} + +// Chart Data Types +export interface ChartDataPoint { label: string; - description: string; - type: 'positive' | 'negative' | 'neutral' | 'event'; + value: number; + color?: string; } -export interface Synthesis { - // Narrative insights for each section - executive_narrative: string; // Main story for exec summary - sentiment_insight: string; // Why sentiment is this way - category_insight: string; // Pattern in categories - timeline_insight: string; // What's changing over time +export interface TimeSeriesPoint { + month: string; // "Jan", "Feb", etc. + month_date: string; // "2025-01" for sorting + value: number; +} - // Highlights and focus areas - priority_domain: string | null; // Domain needing most attention - priority_issue: string | null; // Issue to fix first +export interface DualSeriesPoint { + month: string; + month_date: string; + positive: number; + negative: number; +} - // Actionable recommendations - action_plan: ActionItem[]; // Prioritized actions - issue_actions: Record; // issue_id โ†’ recommended action +export interface ReportCharts { + rating_gauge: { + current: number; + target: number; + min: number; + max: number; + }; + sentiment_pie: ChartDataPoint[]; + issues_pie: ChartDataPoint[]; + rating_distribution: ChartDataPoint[]; + complaints_trend: TimeSeriesPoint[]; + rating_trend: TimeSeriesPoint[]; + momentum_trend: DualSeriesPoint[]; +} - // Timeline context - timeline_annotations: TimelineAnnotation[]; +// Legacy types for backwards compatibility (v1.x reports) +export interface ReportAction { + priority: 'critical' | 'high' | 'medium'; + action: string; + owner: string; + impact: string; + impact_stars: number; + effort: 'quick_win' | 'moderate' | 'strategic'; + evidence: string; + complaint_count: number; + success_metric: string; +} - // Marketing opportunities - marketing_angles: string[]; // Ways to promote strengths +export interface ReportEvidence { + quote: string; + context: string; + sentiment: 'damaging' | 'praising'; + weight: 'critical' | 'notable'; +} - // Competitor context (if available) - competitor_context: string | null; +export interface ReportStrength { + title: string; + mention_count: number; + quote: string; + marketing_angle: string; +} - // Generated at +// Legacy Synthesis Interface (v1.x - for backwards compatibility) +export interface LegacySynthesis { + headline: string; + verdict: string; + current_rating: number; + potential_rating: number; + rating_gap: number; + narrative: string; + sentiment_headline: string; + category_headline: string; + timeline_headline: string; + strengths_headline: string; + primary_problem: string; + primary_problem_code: string; + root_cause: string; + actions: ReportAction[]; + evidence: ReportEvidence[]; + strengths: ReportStrength[]; + momentum: 'improving' | 'declining' | 'stable'; + momentum_detail: string; generated_at: string; + review_count: number; + insight_count: number; +} + +// New Synthesis Interface (v2.0 - 6-Section Report) +export interface SynthesisV2 { + // Report metadata + report_version?: string; // "2.0" + report_title: string; // "Reputation Health Report: Soho Club" + report_date: string; // "January 2026" + business_name: string; + generated_at: string; + review_count: number; + insight_count: number; + analysis_period: string; // "Last 12 months" + + // Section 1: Executive Summary + executive_summary: ExecutiveSummary; + + // Section 2: Risk Scorecard + risk_scorecard: RiskScorecard; + + // Section 3: Critical Issues (Top 3) + critical_issues: CriticalIssue[]; + + // Section 4: Protect Your Strengths + strengths: StrengthToProtect[]; + + // Section 5: Action Matrix + action_matrix: ActionMatrixItem[]; + + // Section 6: 90-Day Tracking Framework + tracking_kpis: TrackingKPI[]; + + // Charts data for visualization + charts?: ReportCharts; +} + +// Union type that supports both formats +export type Synthesis = LegacySynthesis | SynthesisV2; + +// Type guard to check if synthesis is v2 format +export function isSynthesisV2(synthesis: Synthesis): synthesis is SynthesisV2 { + return 'executive_summary' in synthesis && synthesis.executive_summary !== undefined; } // ==================== Issues (Enriched) ==================== @@ -314,6 +469,7 @@ export interface ReviewIQAnalyticsResponse { // ==================== Filter Types ==================== export type TimeRange = '7d' | '14d' | '30d' | '90d' | '1y' | 'all'; +export type Granularity = 'day' | 'week' | 'month' | 'year'; export type Sentiment = 'positive' | 'neutral' | 'negative'; export type URTDomain = 'O' | 'P' | 'J' | 'E' | 'A' | 'V' | 'R'; export type Intensity = 'I1' | 'I2' | 'I3';