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:
@@ -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 */}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useReviewIQFilters } from '@/contexts/ReviewIQFilterContext';
|
||||
|
||||
interface SentimentPieProps {
|
||||
data: SentimentDataPoint[];
|
||||
// AI-generated insight (optional - shows when available)
|
||||
insight?: string | null;
|
||||
}
|
||||
|
||||
// User-friendly sentiment config
|
||||
@@ -74,7 +76,7 @@ const SENTIMENT_ORDER = ['V+', 'V-', 'V0', 'V±'];
|
||||
* User-friendly design with emojis and clear numbers.
|
||||
* Click to filter by sentiment.
|
||||
*/
|
||||
export function SentimentPie({ data }: SentimentPieProps) {
|
||||
export function SentimentPie({ data, insight }: SentimentPieProps) {
|
||||
const { filters, toggleSentiment } = useReviewIQFilters();
|
||||
|
||||
// Process data
|
||||
@@ -185,6 +187,19 @@ export function SentimentPie({ data }: SentimentPieProps) {
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{processedData.cards.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-64 text-gray-500">
|
||||
No sentiment data available
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user