Files
whyrating-engine-legacy/web/components/reviewiq/ReviewIQDashboard.tsx
Alejandro Gutiérrez c8ecb4b98f feat(reviewiq): Add AI synthesis support to dashboard components
Frontend:
- Add Synthesis type with action plan, insights, annotations
- ExecutiveSummary: Accept synthesis prop for AI narrative
- SentimentPie: Accept insight prop for contextual explanation
- IntensityHeatmap: Accept insight + highlightDomain props
- TimelineChart: Accept insight + annotations props
- All components gracefully degrade when synthesis is null

Backend:
- Add Stage 4: Synthesize for generating AI narratives
- Gathers context from classified spans
- Generates executive narrative, section insights, action plan
- Produces timeline annotations and marketing angles
- Stores synthesis in pipeline.executions table

Components show AI insights with purple gradient styling when available,
fall back to existing behavior when synthesis is not yet generated.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 02:59:47 +00:00

184 lines
8.1 KiB
TypeScript

'use client';
import { useState } from 'react';
import { RefreshCw, BarChart3 } 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 { 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';
interface ReviewIQDashboardProps {
jobId?: string | null;
businessId?: string | null;
}
/**
* 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
*/
function ReviewIQDashboardInner({ jobId, businessId }: ReviewIQDashboardProps) {
const { filters, setURTDomain } = useReviewIQFilters();
const [issuesPage, setIssuesPage] = useState(1);
const [spansPage, setSpansPage] = useState(1);
const { data, loading, error, refetch } = useReviewIQAnalytics({
jobId,
businessId,
filters,
issuesPage,
issuesPageSize: 10,
spansPage,
spansPageSize: 10,
});
const handleIssuesPageChange = (page: number) => setIssuesPage(page);
const handleSpansPageChange = (page: number) => setSpansPage(page);
// No job selected
if (!jobId && !businessId) {
return <DashboardEmpty />;
}
// Loading state
if (loading && !data) {
return <DashboardSkeleton />;
}
// Error state
if (error) {
return <DashboardError message={error} onRetry={refetch} />;
}
// No data
if (!data) {
return <DashboardEmpty />;
}
// Handle domain click for filtering
const handleDomainClick = (domain: URTDomain) => {
setURTDomain(filters.urtDomain === domain ? null : domain);
};
return (
<div className="space-y-6">
{/* ═══════════════════════════════════════════════════════════════
HEADER
═══════════════════════════════════════════════════════════════ */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl">
<BarChart3 className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">ReviewIQ Analytics</h1>
<p className="text-sm text-gray-500">
{data.overview.total_reviews.toLocaleString()} reviews {data.overview.total_spans.toLocaleString()} insights extracted
</p>
</div>
</div>
<button
onClick={refetch}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</button>
</div>
{/* Active Filters Bar */}
<FilterBar />
{/* ═══════════════════════════════════════════════════════════════
SECTION 1: EXECUTIVE SUMMARY (Hero)
Rating, AI summary, #1 Problem, #1 Strength, Top Complaints
═══════════════════════════════════════════════════════════════ */}
<ExecutiveSummary
insights={data.insights}
avgRating={data.overview.avg_rating}
domainScores={data.domain_scores}
onDomainClick={handleDomainClick}
synthesis={data.synthesis}
/>
{/* ═══════════════════════════════════════════════════════════════
SECTION 2: EXPLORE (Sentiment + Categories)
Side-by-side: How customers feel + What they talk about
═══════════════════════════════════════════════════════════════ */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<SentimentPie
data={data.sentiment.distribution}
insight={data.synthesis?.sentiment_insight}
/>
<IntensityHeatmap
data={data.urt.domains}
insight={data.synthesis?.category_insight}
highlightDomain={data.synthesis?.priority_domain}
/>
</div>
{/* ═══════════════════════════════════════════════════════════════
SECTION 3: ACTION (Opportunity Matrix)
What to fix - prioritized by impact vs effort
═══════════════════════════════════════════════════════════════ */}
<OpportunityMatrix matrix={data.insights.opportunity_matrix} />
{/* ═══════════════════════════════════════════════════════════════
SECTION 4: TRENDS (Timeline)
How things change over time
═══════════════════════════════════════════════════════════════ */}
<TimelineChart
data={data.timeline}
insight={data.synthesis?.timeline_insight}
annotations={data.synthesis?.timeline_annotations}
/>
{/* ═══════════════════════════════════════════════════════════════
SECTION 5: DEEP DIVE (Tables)
Detailed issues and individual mentions
═══════════════════════════════════════════════════════════════ */}
<div className="grid lg:grid-cols-2 gap-6">
<IssuesTable issues={data.issues} onPageChange={handleIssuesPageChange} />
<SpansTable spans={data.spans} onPageChange={handleSpansPageChange} />
</div>
{/* Debug Info (dev only) */}
{process.env.NODE_ENV === 'development' && (
<details className="bg-gray-100 rounded-lg p-4 text-sm">
<summary className="cursor-pointer font-semibold text-gray-700">
Debug: Filters Applied
</summary>
<pre className="mt-2 bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
{JSON.stringify(data.filters_applied, null, 2)}
</pre>
</details>
)}
</div>
);
}
/**
* Main ReviewIQ Dashboard with filter context provider.
*/
export function ReviewIQDashboard({ jobId, businessId }: ReviewIQDashboardProps) {
return (
<ReviewIQFilterProvider>
<ReviewIQDashboardInner jobId={jobId} businessId={businessId} />
</ReviewIQFilterProvider>
);
}