Files
whyrating-engine-legacy/web/components/reviewiq/tables/IssueDetailModal.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

260 lines
11 KiB
TypeScript

'use client';
import { useState } from 'react';
import { X, AlertTriangle, Layers, Calendar, Target, User, Lightbulb, FileText } from 'lucide-react';
import type { IssueItem, SpanItem } from '../types';
import { DOMAIN_LABELS, INTENSITY_LABELS, VALENCE_LABELS, VALENCE_COLORS } from '../types';
import { useIssueSpans } from '@/hooks/useReviewIQAnalytics';
import { ReviewModal } from './ReviewModal';
interface IssueDetailModalProps {
issue: IssueItem;
onClose: () => void;
}
/**
* Modal showing issue details and related spans.
*/
export function IssueDetailModal({ issue, onClose }: IssueDetailModalProps) {
const { data: spans, loading, error } = useIssueSpans(issue.issue_id);
const [selectedReview, setSelectedReview] = useState<{
reviewId: string;
spanId: string;
} | null>(null);
return (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={onClose}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="sticky top-0 bg-white border-b-2 border-gray-200 px-6 py-4 rounded-t-2xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<AlertTriangle className="w-6 h-6 text-orange-600" />
<div>
<h3 className="text-xl font-bold text-gray-900">
{issue.subcode_name || issue.primary_subcode}
</h3>
<span className="text-sm font-mono text-gray-500">{issue.primary_subcode}</span>
</div>
<span
className={`px-2 py-1 text-xs font-bold rounded border ${
issue.state === 'open'
? 'bg-red-100 text-red-800 border-red-300'
: issue.state === 'resolved'
? 'bg-green-100 text-green-800 border-green-300'
: 'bg-gray-100 text-gray-800 border-gray-300'
}`}
>
{issue.state.toUpperCase()}
</span>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
>
<X className="w-5 h-5 text-gray-600" />
</button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Issue Info Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-purple-50 rounded-lg p-4 border border-purple-200">
<div className="flex items-center gap-2 mb-1">
<Target className="w-4 h-4 text-purple-600" />
<span className="text-xs font-semibold text-purple-700">URT Code</span>
</div>
<span className="text-lg font-mono font-bold text-purple-900">
{issue.primary_subcode}
</span>
</div>
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
<div className="flex items-center gap-2 mb-1">
<Layers className="w-4 h-4 text-blue-600" />
<span className="text-xs font-semibold text-blue-700">Domain</span>
</div>
<span className="text-lg font-bold text-blue-900">
{DOMAIN_LABELS[issue.domain] || issue.domain}
</span>
</div>
<div className="bg-orange-50 rounded-lg p-4 border border-orange-200">
<div className="flex items-center gap-2 mb-1">
<AlertTriangle className="w-4 h-4 text-orange-600" />
<span className="text-xs font-semibold text-orange-700">Priority</span>
</div>
<span className="text-lg font-bold text-orange-900">
{(issue.priority_score * 100).toFixed(0)}%
</span>
</div>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="flex items-center gap-2 mb-1">
<Calendar className="w-4 h-4 text-gray-600" />
<span className="text-xs font-semibold text-gray-700">Intensity</span>
</div>
<span className="text-lg font-bold text-gray-900">
{issue.max_intensity
? INTENSITY_LABELS[issue.max_intensity] || issue.max_intensity
: 'N/A'}
</span>
</div>
</div>
{/* Entity */}
{issue.entity && (
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<span className="text-sm font-semibold text-gray-700">Related Entity</span>
<p className="text-lg font-medium text-gray-900 mt-1">{issue.entity}</p>
</div>
)}
{/* Solution & Owner Section */}
{(issue.solution || issue.default_owner) && (
<div className="bg-green-50 rounded-lg p-4 border border-green-200 space-y-3">
{issue.solution && (
<div>
<div className="flex items-center gap-2 mb-2">
<Lightbulb className="w-5 h-5 text-green-600" />
<span className="text-sm font-bold text-green-800">Recommended Solution</span>
{issue.solution_complexity && (
<span className={`ml-auto text-xs font-bold px-2 py-1 rounded ${
issue.solution_complexity === 'low'
? 'bg-green-100 text-green-700'
: issue.solution_complexity === 'medium'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}>
{issue.solution_complexity.charAt(0).toUpperCase() + issue.solution_complexity.slice(1)} Complexity
</span>
)}
</div>
<p className="text-gray-700 leading-relaxed">{issue.solution}</p>
</div>
)}
{issue.default_owner && (
<div className="flex items-center gap-2 pt-2 border-t border-green-200">
<User className="w-4 h-4 text-green-600" />
<span className="text-sm text-green-800">
<span className="font-medium">Assign to:</span>{' '}
<span className="font-bold">{issue.default_owner}</span>
</span>
</div>
)}
</div>
)}
{/* Related Spans */}
<div>
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-bold text-gray-900">
Related Spans ({issue.span_count})
</h4>
</div>
{loading ? (
<div className="flex items-center justify-center h-32 text-gray-500">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
</div>
) : error ? (
<div className="flex items-center justify-center h-32 text-red-500">
Failed to load spans: {error}
</div>
) : spans.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-500">
No spans found for this issue
</div>
) : (
<div className="space-y-3 max-h-96 overflow-y-auto">
{spans.map((span: SpanItem) => (
<div
key={span.span_id}
className="bg-gray-50 rounded-lg p-4 border border-gray-200"
>
<div className="flex items-start justify-between gap-4">
<p className="text-gray-800 flex-1">{span.span_text}</p>
<div className="flex items-center gap-2 flex-shrink-0">
{span.valence && (
<span
className="px-2 py-1 text-xs font-bold rounded"
style={{
backgroundColor: `${VALENCE_COLORS[span.valence]}20`,
color: VALENCE_COLORS[span.valence],
}}
>
{VALENCE_LABELS[span.valence] || span.valence}
</span>
)}
{span.intensity && (
<span className="px-2 py-1 bg-gray-200 text-gray-700 text-xs font-bold rounded">
{INTENSITY_LABELS[span.intensity] || span.intensity}
</span>
)}
</div>
</div>
<div className="flex items-center justify-between gap-4 mt-2">
<div className="flex items-center gap-4 text-xs text-gray-500">
{span.urt_primary && (
<span className="font-mono">{span.urt_primary}</span>
)}
{span.review_time && (
<span>{new Date(span.review_time).toLocaleDateString()}</span>
)}
{span.entity && <span>Entity: {span.entity}</span>}
</div>
{/* View Full Review Button */}
{span.source_review_id && (
<button
onClick={() =>
setSelectedReview({
reviewId: span.source_review_id!,
spanId: span.span_id,
})
}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-semibold text-blue-600 hover:bg-blue-50 rounded transition-colors"
>
<FileText className="w-3 h-3" />
View Review
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Footer */}
<div className="sticky bottom-0 bg-white border-t-2 border-gray-200 px-6 py-4 rounded-b-2xl">
<button
onClick={onClose}
className="w-full py-3 bg-gray-900 text-white rounded-lg font-semibold hover:bg-gray-800 transition-colors"
>
Close
</button>
</div>
</div>
{/* Review Modal for drill-down */}
{selectedReview && (
<ReviewModal
reviewId={selectedReview.reviewId}
highlightSpanId={selectedReview.spanId}
onClose={() => setSelectedReview(null)}
/>
)}
</div>
);
}