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

@@ -7,6 +7,10 @@ import { useReviewIQFilters } from '@/contexts/ReviewIQFilterContext';
interface SentimentHeatmapProps {
data: URTDomainPoint[];
// AI-generated insight (optional - shows when available)
insight?: string | null;
// Domain to highlight (optional - from AI priority)
highlightDomain?: string | null;
}
// User-friendly domain config with emojis and descriptions
@@ -51,7 +55,7 @@ const getNegativeColor = (value: number, max: number): string => {
* User-friendly design with emojis and clear labels.
* Click to filter by domain and sentiment.
*/
export function IntensityHeatmap({ data }: SentimentHeatmapProps) {
export function IntensityHeatmap({ data, insight, highlightDomain }: SentimentHeatmapProps) {
const { filters, setURTDomain, toggleSentiment } = useReviewIQFilters();
// Check if cross-filters are active
@@ -136,6 +140,19 @@ export function IntensityHeatmap({ data }: SentimentHeatmapProps) {
)}
</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>
)}
{data.length === 0 ? (
<div className="flex items-center justify-center h-64 text-gray-500">
No feedback data available
@@ -170,12 +187,15 @@ export function IntensityHeatmap({ data }: SentimentHeatmapProps) {
const isDomainActive = filters.urtDomain === row.domain;
const isPositiveActive = isDomainActive && filters.sentiment.includes('positive');
const isNegativeActive = isDomainActive && filters.sentiment.includes('negative');
const isHighlighted = highlightDomain === row.domain;
return (
<tr
key={row.domain}
className={`border-t border-gray-100 transition-colors ${
isDomainActive ? 'bg-blue-50' : 'hover:bg-gray-50'
isDomainActive ? 'bg-blue-50' :
isHighlighted ? 'bg-purple-50 ring-1 ring-purple-300' :
'hover:bg-gray-50'
}`}
>
{/* Domain Label */}