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:
259
web/components/reviewiq/tables/IssueDetailModal.tsx
Normal file
259
web/components/reviewiq/tables/IssueDetailModal.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user