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>
This commit is contained in:
120
web/components/reviewiq/kpi/KPISection.tsx
Normal file
120
web/components/reviewiq/kpi/KPISection.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
MessageSquare,
|
||||
AlertTriangle,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Star,
|
||||
Target,
|
||||
Layers,
|
||||
TrendingUp,
|
||||
} from 'lucide-react';
|
||||
import { KPICard } from './KPICard';
|
||||
import type { OverviewStats, Sentiment } from '../types';
|
||||
import { useReviewIQFilters } from '@/contexts/ReviewIQFilterContext';
|
||||
|
||||
interface KPISectionProps {
|
||||
overview: OverviewStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* KPI cards section showing overview statistics.
|
||||
* Cards are clickable to filter the dashboard.
|
||||
*/
|
||||
export function KPISection({ overview }: KPISectionProps) {
|
||||
const { filters, toggleSentiment } = useReviewIQFilters();
|
||||
|
||||
const positiveActive = filters.sentiment.includes('positive');
|
||||
const negativeActive = filters.sentiment.includes('negative');
|
||||
|
||||
const totalSentiment =
|
||||
overview.positive_count + overview.negative_count + overview.neutral_count + overview.mixed_count;
|
||||
|
||||
const positivePercent =
|
||||
totalSentiment > 0 ? ((overview.positive_count / totalSentiment) * 100).toFixed(0) : '0';
|
||||
const negativePercent =
|
||||
totalSentiment > 0 ? ((overview.negative_count / totalSentiment) * 100).toFixed(0) : '0';
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{/* Total Reviews */}
|
||||
<KPICard
|
||||
title="Reviews"
|
||||
value={overview.total_reviews}
|
||||
subtitle="Total processed"
|
||||
icon={MessageSquare}
|
||||
colorClass="bg-gradient-to-br from-blue-100 to-blue-200 border-blue-400 text-blue-900"
|
||||
/>
|
||||
|
||||
{/* Total Spans */}
|
||||
<KPICard
|
||||
title="Spans"
|
||||
value={overview.total_spans}
|
||||
subtitle="Classified segments"
|
||||
icon={Layers}
|
||||
colorClass="bg-gradient-to-br from-purple-100 to-purple-200 border-purple-400 text-purple-900"
|
||||
/>
|
||||
|
||||
{/* Open Issues */}
|
||||
<KPICard
|
||||
title="Issues"
|
||||
value={overview.open_issues}
|
||||
subtitle="Open issues"
|
||||
icon={AlertTriangle}
|
||||
colorClass="bg-gradient-to-br from-orange-100 to-orange-200 border-orange-400 text-orange-900"
|
||||
/>
|
||||
|
||||
{/* Average Rating */}
|
||||
<KPICard
|
||||
title="Avg Rating"
|
||||
value={overview.avg_rating !== null ? `${overview.avg_rating.toFixed(1)}` : 'N/A'}
|
||||
subtitle="Star rating"
|
||||
icon={Star}
|
||||
colorClass="bg-gradient-to-br from-yellow-100 to-yellow-200 border-yellow-400 text-yellow-900"
|
||||
/>
|
||||
|
||||
{/* Positive Count */}
|
||||
<KPICard
|
||||
title="Positive"
|
||||
value={overview.positive_count}
|
||||
subtitle={`${positivePercent}% of mentions`}
|
||||
icon={ThumbsUp}
|
||||
colorClass="bg-gradient-to-br from-green-100 to-green-200 border-green-400 text-green-900"
|
||||
onClick={() => toggleSentiment('positive')}
|
||||
isActive={positiveActive}
|
||||
/>
|
||||
|
||||
{/* Negative Count */}
|
||||
<KPICard
|
||||
title="Negative"
|
||||
value={overview.negative_count}
|
||||
subtitle={`${negativePercent}% of mentions`}
|
||||
icon={ThumbsDown}
|
||||
colorClass="bg-gradient-to-br from-red-100 to-red-200 border-red-400 text-red-900"
|
||||
onClick={() => toggleSentiment('negative')}
|
||||
isActive={negativeActive}
|
||||
/>
|
||||
|
||||
{/* Neutral Count */}
|
||||
<KPICard
|
||||
title="Neutral"
|
||||
value={overview.neutral_count}
|
||||
subtitle="Neutral mentions"
|
||||
icon={Target}
|
||||
colorClass="bg-gradient-to-br from-gray-100 to-gray-200 border-gray-400 text-gray-900"
|
||||
onClick={() => toggleSentiment('neutral')}
|
||||
isActive={filters.sentiment.includes('neutral')}
|
||||
/>
|
||||
|
||||
{/* Mixed Count */}
|
||||
<KPICard
|
||||
title="Mixed"
|
||||
value={overview.mixed_count}
|
||||
subtitle="Mixed mentions"
|
||||
icon={TrendingUp}
|
||||
colorClass="bg-gradient-to-br from-amber-100 to-amber-200 border-amber-400 text-amber-900"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user