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:
Alejandro Gutiérrez
2026-01-29 02:59:47 +00:00
parent 8f9dd136cd
commit c8ecb4b98f
21 changed files with 3959 additions and 90 deletions

View File

@@ -15,12 +15,16 @@ import {
ReferenceLine,
} from 'recharts';
import { X, TrendingUp, TrendingDown, Minus, Calendar, Filter } from 'lucide-react';
import type { TimelinePoint, TimeRange } from '../types';
import type { TimelinePoint, TimeRange, TimelineAnnotation } from '../types';
import { DOMAIN_LABELS } from '../types';
import { useReviewIQFilters } from '@/contexts/ReviewIQFilterContext';
interface TimelineChartProps {
data: TimelinePoint[];
// AI-generated insight (optional - shows when available)
insight?: string | null;
// Timeline annotations from AI (optional - marks key events)
annotations?: TimelineAnnotation[] | null;
}
type ViewMode = 'sentiment' | 'volume' | 'rating';
@@ -45,7 +49,7 @@ const TIME_RANGE_OPTIONS: { value: TimeRange; label: string; description: string
* User-friendly design with view toggles and interactive brush.
* Responds to domain/sentiment filters.
*/
export function TimelineChart({ data }: TimelineChartProps) {
export function TimelineChart({ data, insight, annotations }: TimelineChartProps) {
const { filters, setTimeRange, setBrushRange } = useReviewIQFilters();
const [viewMode, setViewMode] = useState<ViewMode>('sentiment');
const [localBrushRange, setLocalBrushRange] = useState<{
@@ -264,6 +268,44 @@ export function TimelineChart({ data }: TimelineChartProps) {
)}
</div>
{/* AI Insight (when available) */}
{insight && (
<div className="mb-4 p-3 bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg border border-purple-200">
<div className="flex items-start gap-2">
<span className="text-sm"></span>
<div>
<div className="text-xs font-medium text-purple-600 mb-0.5">AI Insight</div>
<p className="text-sm text-gray-700">{insight}</p>
</div>
</div>
</div>
)}
{/* Key Events (when annotations available) */}
{annotations && annotations.length > 0 && (
<div className="mb-4 flex flex-wrap gap-2">
{annotations.slice(0, 3).map((annotation, idx) => (
<div
key={idx}
className={`px-3 py-1.5 rounded-full text-xs font-medium flex items-center gap-1 ${
annotation.type === 'positive' ? 'bg-green-100 text-green-700' :
annotation.type === 'negative' ? 'bg-red-100 text-red-700' :
annotation.type === 'event' ? 'bg-blue-100 text-blue-700' :
'bg-gray-100 text-gray-700'
}`}
title={annotation.description}
>
<span>{
annotation.type === 'positive' ? '📈' :
annotation.type === 'negative' ? '📉' :
annotation.type === 'event' ? '📍' : '•'
}</span>
<span>{annotation.label}</span>
</div>
))}
</div>
)}
{sortedData.length === 0 ? (
<div className="flex flex-col items-center justify-center h-80 text-gray-500">
<Calendar className="w-12 h-12 text-gray-300 mb-2" />