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

@@ -16,7 +16,7 @@ import {
Award,
} from 'lucide-react';
import { useTranslation } from '@/hooks/useTranslation';
import type { Insights, WeaknessItem, OpportunitySpan, OpportunityMatrix, DomainScore, URTDomain } from '../types';
import type { Insights, WeaknessItem, OpportunitySpan, OpportunityMatrix, DomainScore, URTDomain, Synthesis } from '../types';
import { getSubcodeDefinition } from '@/lib/taxonomy/data';
interface ExecutiveSummaryProps {
@@ -25,6 +25,8 @@ interface ExecutiveSummaryProps {
domainScores?: DomainScore[];
onDriverClick?: (subcode: string) => void;
onDomainClick?: (domain: URTDomain) => void;
// AI-generated narrative (optional - enhances when available)
synthesis?: Synthesis | null;
}
// User-friendly domain config
@@ -199,10 +201,14 @@ export function ExecutiveSummary({
domainScores,
onDriverClick,
onDomainClick,
synthesis,
}: ExecutiveSummaryProps) {
const { strengths, weaknesses, executive_summary, opportunity_matrix, rating_simulator } = insights;
const [showFullSummary, setShowFullSummary] = useState(false);
// Use AI narrative if available, otherwise fall back to generated summary
const narrativeText = synthesis?.executive_narrative || executive_summary;
const topStrength = strengths[0];
const topWeakness = weaknesses[0];
const ratingDisplay = getRatingDisplay(avgRating);
@@ -286,16 +292,23 @@ export function ExecutiveSummary({
</div>
{/* AI Summary */}
{executive_summary && (
{narrativeText && (
<div className="px-6 pb-4">
<div className="p-4 bg-white/70 rounded-xl border border-blue-100">
<div className={`p-4 rounded-xl border ${
synthesis?.executive_narrative
? 'bg-gradient-to-r from-purple-50 to-blue-50 border-purple-200'
: 'bg-white/70 border-blue-100'
}`}>
<div className="flex items-start gap-2">
<span className="text-lg">💡</span>
<div>
<p className={`text-gray-700 leading-relaxed ${!showFullSummary && 'line-clamp-2'}`}>
{executive_summary}
<span className="text-lg">{synthesis?.executive_narrative ? '✨' : '💡'}</span>
<div className="flex-1">
{synthesis?.executive_narrative && (
<div className="text-xs font-medium text-purple-600 mb-1">AI-Generated Insight</div>
)}
<p className={`text-gray-700 leading-relaxed ${!showFullSummary && 'line-clamp-3'}`}>
{narrativeText}
</p>
{executive_summary.length > 150 && (
{narrativeText.length > 200 && (
<button
onClick={() => setShowFullSummary(!showFullSummary)}
className="text-blue-600 text-sm font-medium mt-1 hover:underline"