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>
186 lines
7.6 KiB
TypeScript
186 lines
7.6 KiB
TypeScript
'use client';
|
|
|
|
import { TrendingUp, TrendingDown, Lightbulb, Target, Megaphone, User } from 'lucide-react';
|
|
import type { StrengthItem, WeaknessItem } from '../types';
|
|
import { DOMAIN_COLORS, COMPLEXITY_LABELS } from '../types';
|
|
import { getSubcodeDefinition } from '@/lib/taxonomy/data';
|
|
|
|
interface StrengthsWeaknessesProps {
|
|
strengths: StrengthItem[];
|
|
weaknesses: WeaknessItem[];
|
|
onStrengthClick?: (subcode: string) => void;
|
|
onWeaknessClick?: (subcode: string) => void;
|
|
}
|
|
|
|
export function StrengthsWeaknesses({
|
|
strengths,
|
|
weaknesses,
|
|
onStrengthClick,
|
|
onWeaknessClick,
|
|
}: StrengthsWeaknessesProps) {
|
|
const hasData = strengths.length > 0 || weaknesses.length > 0;
|
|
|
|
if (!hasData) {
|
|
return (
|
|
<div className="bg-white rounded-lg border border-gray-200 p-6 text-center text-gray-500">
|
|
<p>Not enough data to identify strengths and weaknesses.</p>
|
|
<p className="text-sm mt-1">More reviews are needed for analysis.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{/* Strengths Panel */}
|
|
<div className="bg-white rounded-lg border border-green-200 overflow-hidden">
|
|
<div className="bg-green-50 px-4 py-3 border-b border-green-200">
|
|
<div className="flex items-center gap-2">
|
|
<TrendingUp className="w-5 h-5 text-green-600" />
|
|
<h3 className="font-semibold text-green-800">Your Strengths</h3>
|
|
</div>
|
|
<p className="text-xs text-green-600 mt-0.5">Protect & amplify these</p>
|
|
</div>
|
|
|
|
<div className="divide-y divide-green-100">
|
|
{strengths.length === 0 ? (
|
|
<div className="p-4 text-center text-gray-500 text-sm">
|
|
No strong positive patterns detected yet.
|
|
</div>
|
|
) : (
|
|
strengths.map((strength) => (
|
|
<button
|
|
key={strength.subcode}
|
|
onClick={() => onStrengthClick?.(strength.subcode)}
|
|
className="w-full p-3 text-left hover:bg-green-50 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-gray-900">
|
|
{strength.rank}. {strength.subcode_name}
|
|
</span>
|
|
<span
|
|
className="text-xs px-1.5 py-0.5 rounded"
|
|
style={{
|
|
backgroundColor: `${DOMAIN_COLORS[strength.domain]}20`,
|
|
color: DOMAIN_COLORS[strength.domain],
|
|
}}
|
|
>
|
|
{strength.domain}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-gray-400 italic">
|
|
{getSubcodeDefinition(strength.subcode) || strength.subcode_name}
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">
|
|
{strength.positive_percentage.toFixed(0)}% positive, {strength.span_count} mentions
|
|
</div>
|
|
</div>
|
|
<div className="text-green-600 font-bold text-lg">
|
|
{strength.positive_percentage.toFixed(0)}%
|
|
</div>
|
|
</div>
|
|
|
|
{strength.marketing_angle && (
|
|
<div className="mt-2 flex items-start gap-1.5 text-sm text-green-700 bg-green-50 rounded p-2">
|
|
<Megaphone className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
<span>{strength.marketing_angle}</span>
|
|
</div>
|
|
)}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Weaknesses Panel */}
|
|
<div className="bg-white rounded-lg border border-red-200 overflow-hidden">
|
|
<div className="bg-red-50 px-4 py-3 border-b border-red-200">
|
|
<div className="flex items-center gap-2">
|
|
<TrendingDown className="w-5 h-5 text-red-600" />
|
|
<h3 className="font-semibold text-red-800">Areas to Improve</h3>
|
|
</div>
|
|
<p className="text-xs text-red-600 mt-0.5">Fix these to boost rating</p>
|
|
</div>
|
|
|
|
<div className="divide-y divide-red-100">
|
|
{weaknesses.length === 0 ? (
|
|
<div className="p-4 text-center text-gray-500 text-sm">
|
|
No significant issues detected. Great job!
|
|
</div>
|
|
) : (
|
|
weaknesses.map((weakness) => (
|
|
<button
|
|
key={weakness.subcode}
|
|
onClick={() => onWeaknessClick?.(weakness.subcode)}
|
|
className="w-full p-3 text-left hover:bg-red-50 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-gray-900">
|
|
{weakness.rank}. {weakness.subcode_name}
|
|
</span>
|
|
<span
|
|
className="text-xs px-1.5 py-0.5 rounded"
|
|
style={{
|
|
backgroundColor: `${DOMAIN_COLORS[weakness.domain]}20`,
|
|
color: DOMAIN_COLORS[weakness.domain],
|
|
}}
|
|
>
|
|
{weakness.domain}
|
|
</span>
|
|
{weakness.intensity === 'I3' && (
|
|
<span className="text-xs px-1.5 py-0.5 rounded bg-red-100 text-red-700">
|
|
High Intensity
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-gray-400 italic">
|
|
{getSubcodeDefinition(weakness.subcode) || weakness.subcode_name}
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">
|
|
{weakness.negative_percentage.toFixed(0)}% negative, {weakness.span_count} mentions
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-red-600 font-bold text-lg">
|
|
{weakness.negative_percentage.toFixed(0)}%
|
|
</div>
|
|
{weakness.projected_rating_impact && (
|
|
<div className="text-xs text-green-600">
|
|
+{weakness.projected_rating_impact.toFixed(2)} if fixed
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{weakness.solution && (
|
|
<div className="mt-2 flex items-start gap-1.5 text-sm text-amber-700 bg-amber-50 rounded p-2">
|
|
<Lightbulb className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
<span>{weakness.solution}</span>
|
|
</div>
|
|
)}
|
|
|
|
{weakness.owner && (
|
|
<div className="mt-1.5 flex items-center gap-1 text-xs text-gray-500">
|
|
<User className="w-3 h-3" />
|
|
<span>Owner: {weakness.owner}</span>
|
|
{weakness.solution_complexity && (
|
|
<>
|
|
<span className="mx-1">|</span>
|
|
<Target className="w-3 h-3" />
|
|
<span>{COMPLEXITY_LABELS[weakness.solution_complexity] || weakness.solution_complexity}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|