Files
whyrating-engine-legacy/web/components/reviewiq/insights/RatingSimulator.tsx
Alejandro Gutiérrez c8ecb4b98f 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>
2026-01-29 02:59:47 +00:00

205 lines
7.4 KiB
TypeScript

'use client';
import { Star, TrendingUp, Award, Zap } from 'lucide-react';
import type { RatingSimulator as RatingSimulatorType, WeaknessItem } from '../types';
interface RatingSimulatorProps {
simulator: RatingSimulatorType | null;
topWeaknesses?: WeaknessItem[];
}
interface RatingStarProps {
rating: number;
label: string;
color: string;
isProjected?: boolean;
}
function RatingDisplay({ rating, label, color, isProjected }: RatingStarProps) {
const fullStars = Math.floor(rating);
const partialStar = rating - fullStars;
const emptyStars = 5 - Math.ceil(rating);
return (
<div className="text-center">
<div className="text-xs font-semibold text-gray-500 mb-1">{label}</div>
<div className="flex items-center justify-center gap-0.5 mb-1">
{/* Full stars */}
{Array.from({ length: fullStars }).map((_, i) => (
<Star
key={`full-${i}`}
className="w-5 h-5"
style={{ fill: color, stroke: color }}
/>
))}
{/* Partial star */}
{partialStar > 0 && (
<div className="relative w-5 h-5">
<Star className="absolute w-5 h-5 text-gray-300" />
<div
className="absolute overflow-hidden"
style={{ width: `${partialStar * 100}%` }}
>
<Star
className="w-5 h-5"
style={{ fill: color, stroke: color }}
/>
</div>
</div>
)}
{/* Empty stars */}
{Array.from({ length: emptyStars }).map((_, i) => (
<Star key={`empty-${i}`} className="w-5 h-5 text-gray-300" />
))}
</div>
<div className={`text-2xl font-bold ${isProjected ? 'text-green-600' : 'text-gray-900'}`}>
{rating.toFixed(2)}
</div>
</div>
);
}
/**
* Rating simulator showing potential rating improvements.
*/
export function RatingSimulator({ simulator, topWeaknesses = [] }: RatingSimulatorProps) {
if (!simulator || simulator.potential_gain <= 0) {
return null;
}
const { current_rating, if_fix_top_1, if_fix_top_3, potential_gain } = simulator;
// Calculate progress towards 5 stars
const currentProgress = (current_rating / 5) * 100;
const potentialProgress = ((current_rating + potential_gain) / 5) * 100;
return (
<div className="bg-gradient-to-br from-yellow-50 via-white to-green-50 rounded-xl p-6 shadow-md border-2 border-yellow-200">
<div className="flex items-center gap-2 mb-4">
<div className="p-2 bg-yellow-100 rounded-lg">
<Star className="w-5 h-5 text-yellow-600" fill="#ca8a04" />
</div>
<h3 className="text-lg font-bold text-gray-900">Rating Simulator</h3>
<span className="ml-auto flex items-center gap-1 px-2 py-1 bg-green-100 text-green-700 text-sm font-bold rounded-full">
<TrendingUp className="w-4 h-4" />
+{potential_gain.toFixed(2)} potential
</span>
</div>
{/* Rating Comparisons */}
<div className="grid grid-cols-3 gap-4 mb-6">
<RatingDisplay
rating={current_rating}
label="Current"
color="#eab308"
/>
{if_fix_top_1 && (
<RatingDisplay
rating={if_fix_top_1}
label="Fix #1 Issue"
color="#22c55e"
isProjected
/>
)}
{if_fix_top_3 && (
<RatingDisplay
rating={if_fix_top_3}
label="Fix Top 3"
color="#10b981"
isProjected
/>
)}
</div>
{/* Progress Bar */}
<div className="mb-4">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>Progress to 5 Stars</span>
<span>{Math.min(100, potentialProgress).toFixed(0)}% achievable</span>
</div>
<div className="h-3 bg-gray-200 rounded-full overflow-hidden relative">
{/* Current rating progress */}
<div
className="absolute h-full bg-gradient-to-r from-yellow-400 to-yellow-500 transition-all duration-500"
style={{ width: `${currentProgress}%` }}
/>
{/* Potential gain overlay */}
<div
className="absolute h-full bg-gradient-to-r from-green-400/50 to-green-500/50 transition-all duration-500"
style={{
left: `${currentProgress}%`,
width: `${Math.min(100 - currentProgress, (potential_gain / 5) * 100)}%`,
}}
/>
</div>
<div className="flex justify-between text-xs mt-1">
<span className="text-gray-400">1</span>
<span className="text-gray-400">2</span>
<span className="text-gray-400">3</span>
<span className="text-gray-400">4</span>
<span className="text-gray-400">5</span>
</div>
</div>
{/* Action Items */}
{topWeaknesses.length > 0 && (
<div className="border-t border-yellow-200 pt-4">
<div className="flex items-center gap-2 mb-2">
<Zap className="w-4 h-4 text-orange-500" />
<span className="text-sm font-semibold text-gray-700">Priority Fixes</span>
</div>
<div className="space-y-2">
{topWeaknesses.slice(0, 3).map((weakness, index) => (
<div
key={weakness.subcode}
className="flex items-center gap-2 p-2 bg-white rounded-lg border border-gray-200"
>
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
index === 0 ? 'bg-red-100 text-red-700' :
index === 1 ? 'bg-orange-100 text-orange-700' :
'bg-yellow-100 text-yellow-700'
}`}>
{index + 1}
</div>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-gray-900 truncate block">
{weakness.subcode_name}
</span>
<span className="text-xs text-gray-500">
{weakness.negative_percentage.toFixed(0)}% negative
{weakness.projected_rating_impact && (
<span className="ml-2 text-green-600 font-semibold">
+{weakness.projected_rating_impact.toFixed(2)} if fixed
</span>
)}
</span>
</div>
{weakness.solution_complexity && (
<span className={`text-xs font-semibold px-2 py-0.5 rounded ${
weakness.solution_complexity === 'simple' ? 'bg-green-100 text-green-700' :
weakness.solution_complexity === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
{weakness.solution_complexity}
</span>
)}
</div>
))}
</div>
</div>
)}
{/* CTA */}
<div className="mt-4 p-3 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-center gap-2">
<Award className="w-5 h-5 text-green-600" />
<span className="text-sm font-medium text-green-800">
Fixing the top 3 issues could boost your rating by{' '}
<span className="font-bold">{((if_fix_top_3 || current_rating) - current_rating).toFixed(2)} stars</span>
</span>
</div>
</div>
</div>
);
}