ai-helpdesk-api / Frontend /src /user /pages /AIUnderstanding.jsx
ritesh19180's picture
Upload folder using huggingface_hub
d96d2a2 verified
Raw
History Blame Contribute Delete
26.6 kB
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Bot, Layers, Tag, AlertCircle, ShieldCheck, Zap, ArrowRight,
Activity, FileText, BrainCircuit, LayoutGrid, CheckCircle2,
ImageIcon, ChevronDown, Lightbulb
} from 'lucide-react';
import useTicketStore from "../../store/ticketStore";
import { Card, CardContent } from "../../components/ui/card";
// ─── Shimmer Skeleton ────────────────────────────────────────────────
const Shimmer = ({ className = "" }) => (
<div className={`relative overflow-hidden rounded-lg bg-gray-100 ${className}`}>
<div className="absolute inset-0 -translate-x-full animate-[shimmer_1.5s_infinite] bg-gradient-to-r from-transparent via-white/60 to-transparent" />
</div>
);
const SkeletonLoader = () => (
<div className="min-h-screen bg-[#f6f8f7] pb-20 pt-32 px-6">
<div className="w-full max-w-[900px] mx-auto space-y-8">
{/* Header skeleton */}
<div className="text-center space-y-3">
<Shimmer className="h-8 w-72 mx-auto" />
<Shimmer className="h-4 w-96 mx-auto" />
</div>
{/* Pipeline skeleton */}
<div className="flex items-center justify-center gap-4 py-4">
{[1, 2, 3, 4].map(i => (
<React.Fragment key={i}>
<div className="flex flex-col items-center gap-2">
<Shimmer className="w-10 h-10 rounded-full" />
<Shimmer className="h-3 w-16" />
</div>
{i < 4 && <Shimmer className="h-0.5 w-12 mt-[-20px]" />}
</React.Fragment>
))}
</div>
{/* Summary skeleton */}
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white">
<CardContent className="p-8 flex items-start gap-6">
<Shimmer className="w-14 h-14 rounded-full shrink-0" />
<div className="flex-1 space-y-3">
<Shimmer className="h-3 w-32" />
<Shimmer className="h-5 w-full" />
<Shimmer className="h-5 w-3/4" />
</div>
</CardContent>
</Card>
{/* Grid skeleton */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white">
<CardContent className="p-8 space-y-5">
<Shimmer className="h-4 w-32" />
<Shimmer className="h-8 w-40" />
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="space-y-2"><Shimmer className="h-3 w-16" /><Shimmer className="h-6 w-20" /></div>
<div className="space-y-2"><Shimmer className="h-3 w-24" /><Shimmer className="h-6 w-28" /></div>
</div>
</CardContent>
</Card>
<div className="space-y-8">
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white">
<CardContent className="p-8 space-y-4">
<Shimmer className="h-4 w-48" />
<div className="flex flex-wrap gap-2">
<Shimmer className="h-7 w-20 rounded-full" />
<Shimmer className="h-7 w-24 rounded-full" />
<Shimmer className="h-7 w-28 rounded-full" />
</div>
</CardContent>
</Card>
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white">
<CardContent className="p-8 space-y-3">
<Shimmer className="h-4 w-40" />
<Shimmer className="h-2.5 w-full rounded-full" />
</CardContent>
</Card>
</div>
</div>
</div>
</div>
);
// ─── Main Component ──────────────────────────────────────────────────
const AIUnderstanding = () => {
const navigate = useNavigate();
const aiTicket = useTicketStore((state) => state.aiTicket);
const setAITicket = useTicketStore((state) => state.setAITicket);
const [isLoading, setIsLoading] = useState(true);
const [editedIssue, setEditedIssue] = useState("");
const [explainerOpen, setExplainerOpen] = useState(false);
// Simulate a brief load to allow store hydration
useEffect(() => {
const timer = setTimeout(() => setIsLoading(false), 600);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (!isLoading && !aiTicket) {
navigate('/create-ticket');
}
}, [isLoading, aiTicket, navigate]);
useEffect(() => {
if (aiTicket?.originalIssue) {
setEditedIssue(aiTicket.originalIssue);
}
}, [aiTicket]);
if (isLoading) return <SkeletonLoader />;
if (!aiTicket) return null;
// ── Dynamic fields from store ──
const summary = aiTicket.summary || aiTicket.originalIssue || "No issue text provided.";
const category = aiTicket.category || "Uncategorized";
const subcategory = aiTicket.subcategory || "None";
const priority = aiTicket.priority || "Medium";
const assignedTeam = aiTicket.assigned_team || "Triage";
const entities = aiTicket.entities || [];
const rawConfidence = aiTicket.confidence;
const confidence = rawConfidence != null ? Math.round(rawConfidence * 100) : 95;
let confBarColor = "bg-red-500";
if (confidence >= 90) confBarColor = "bg-emerald-500";
else if (confidence >= 70) confBarColor = "bg-amber-500";
const priorityLower = (priority || 'medium').toLowerCase();
let priorityColor = "bg-slate-100 text-slate-700";
if (priorityLower === 'high' || priorityLower === 'critical') priorityColor = "bg-red-50 text-red-700";
else if (priorityLower === 'medium') priorityColor = "bg-amber-50 text-amber-700";
else if (priorityLower === 'low') priorityColor = "bg-emerald-50 text-emerald-700";
const handleUpdate = () => {
setAITicket({ ...aiTicket, originalIssue: editedIssue });
};
const handleContinue = () => {
setAITicket({ ...aiTicket, originalIssue: editedIssue, status: 'duplicate_check' });
navigate('/knowledge-check');
};
// ── Pipeline stages ──
const pipelineStages = [
{ label: 'Input', icon: FileText },
{ label: 'AI Analysis', icon: BrainCircuit },
{ label: 'Classification', icon: LayoutGrid },
{ label: 'Decision', icon: CheckCircle2 },
];
const currentStage = aiTicket?.status === 'duplicate_check' ? 2 : (aiTicket?.status === 'analyzing' ? 1 : 0);
// ── Group entities by label ──
const groupedEntities = entities.reduce((acc, entity) => {
const label = entity.label || 'Other';
if (!acc[label]) acc[label] = [];
acc[label].push(entity.text);
return acc;
}, {});
// ── Explainer data ──
const signalTexts = entities.map(e => e.text);
const patternMatch = `${category} > ${subcategory}`;
const safeCategory = String(category || 'General').toLowerCase();
const safeSubcategory = String(subcategory || 'Support').toLowerCase();
const confidenceExplanation = confidence >= 90
? `This issue strongly matches known ${safeCategory} ${safeSubcategory} failures.`
: confidence >= 70
? `This issue partially matches patterns in ${safeCategory} issues.`
: `This issue has a weak match; further review may be needed.`;
return (
<div className="min-h-screen bg-[#f6f8f7] pb-20 pt-32 px-6">
<style>{`
@keyframes shimmer {
100% { transform: translateX(100%); }
}
`}</style>
<div className="w-full max-w-[900px] mx-auto space-y-8">
{/* 1. Page Header */}
<div className="mb-2 text-center">
<h1 className="text-3xl font-black text-gray-900 tracking-tight">AI Analysis Complete</h1>
<p className="text-gray-500 font-medium mt-2">
Our AI analyzed your request and identified the issue.
</p>
</div>
{/* Horizontal Analysis Pipeline */}
<div className="flex items-center justify-center gap-0 py-4">
{pipelineStages.map((stage, idx) => {
const Icon = stage.icon;
const isCompleted = idx < currentStage;
const isCurrent = idx === currentStage;
return (
<React.Fragment key={stage.label}>
<div className="flex flex-col items-center gap-2 min-w-[90px]">
<div className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${isCurrent
? 'bg-emerald-500 text-white shadow-lg shadow-emerald-200'
: isCompleted
? 'bg-emerald-100 text-emerald-600'
: 'bg-gray-100 text-gray-400'
}`}>
<Icon className="w-5 h-5" />
</div>
<span className={`text-xs font-bold tracking-tight ${isCurrent ? 'text-emerald-600' : isCompleted ? 'text-gray-700' : 'text-gray-400'
}`}>
{stage.label}
</span>
</div>
{idx < pipelineStages.length - 1 && (
<div className={`flex-1 h-0.5 max-w-[60px] rounded-full mx-1 mt-[-20px] ${idx < currentStage ? 'bg-emerald-400' : 'bg-gray-200'
}`} />
)}
</React.Fragment>
);
})}
</div>
{/* 2. Issue Summary Card */}
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white overflow-hidden">
<CardContent className="p-8 flex items-start gap-6">
<div className="w-14 h-14 rounded-full bg-emerald-50 flex items-center justify-center shrink-0">
<Bot className="w-7 h-7 text-emerald-600" />
</div>
<div className="flex-1">
<h2 className="text-xs font-black text-gray-400 uppercase tracking-widest mb-2">AI Summary</h2>
<p className="text-lg font-bold text-gray-900 leading-relaxed">
{summary}
</p>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 3. AI Classification Card */}
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white">
<CardContent className="p-8 space-y-6">
<h3 className="text-sm font-black text-gray-900 flex items-center gap-2">
<Layers className="w-4 h-4 text-emerald-500" />
Classification
</h3>
<div className="space-y-4">
<div>
<p className="text-xs font-semibold text-gray-400 mb-1">Category & Subcategory</p>
<div className="flex items-center gap-2">
<span className="px-3 py-1 rounded-full bg-emerald-50 text-emerald-700 text-sm font-bold border border-emerald-100">
{category}
</span>
<span className="text-gray-300">/</span>
<span className="text-sm font-medium text-gray-600">
{subcategory}
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-2">
<div>
<p className="text-xs font-semibold text-gray-400 mb-1">Priority</p>
<span className={`px-3 py-1 rounded-full text-xs font-bold ${priorityColor}`}>
{priority}
</span>
</div>
<div>
<p className="text-xs font-semibold text-gray-400 mb-1">Assigned Team</p>
<span className="flex items-center gap-1.5 text-sm font-semibold text-gray-700">
<ShieldCheck className="w-4 h-4 text-emerald-500" />
{assignedTeam}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 4. Entities & 5. Confidence */}
<div className="space-y-8">
{/* Entities Extracted — Grouped by Type */}
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white">
<CardContent className="p-8">
<h3 className="text-sm font-black text-gray-900 flex items-center gap-2 mb-5">
<Zap className="w-4 h-4 text-amber-500" />
Technical Signals Detected
</h3>
{Object.keys(groupedEntities).length > 0 ? (
<div className="space-y-4">
{Object.entries(groupedEntities).map(([label, texts]) => (
<div key={label}>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2">{label}</p>
<div className="flex flex-wrap gap-2">
{texts.map((t, idx) => (
<span key={idx} className="px-3 py-1 rounded-full bg-white border border-emerald-200 text-gray-700 text-xs font-bold flex items-center gap-1.5">
<Tag className="w-3 h-3 text-emerald-400" />
{t}
</span>
))}
</div>
</div>
))}
</div>
) : (
<span className="text-sm text-gray-500 italic">No specific signals detected.</span>
)}
{/* Technical Environment — User visible diagnostic */}
{aiTicket.env_metadata && (
<div className="mt-6 p-4 rounded-xl border border-indigo-100 bg-indigo-50/30">
<h4 className="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
<ShieldCheck className="w-3.5 h-3.5" />
Technical Environment
</h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-wider">Interface IP</span>
<span className="text-[10px] font-black text-indigo-600 font-mono italic">
{aiTicket.env_metadata.ip || '127.0.0.1'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-wider">Device Signal</span>
<span className="text-[10px] font-medium text-gray-500 truncate ml-4 italic">
{aiTicket.env_metadata.user_agent ? aiTicket.env_metadata.user_agent.split(' ').slice(0, 3).join(' ') + '...' : 'SECURE_NODE'}
</span>
</div>
</div>
</div>
)}
{/* OCR Screenshot Entities */}
{aiTicket.ocrText && (
<div className="mt-6 p-4 rounded-xl border border-dashed border-gray-200 bg-gray-50/50">
<h4 className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
<ImageIcon className="w-3.5 h-3.5" />
Detected from Screenshot
</h4>
<div className="flex flex-wrap gap-2">
{aiTicket.ocrText.split(/\n|,/).filter(Boolean).map((line, idx) => {
const trimmed = line.trim();
if (!trimmed) return null;
return (
<span key={idx} className="px-3 py-1 rounded-full bg-white border border-emerald-200 text-gray-700 text-sm font-bold">
{trimmed}
</span>
);
})}
</div>
</div>
)}
</CardContent>
</Card>
{/* Confidence Meter */}
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white">
<CardContent className="p-8">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-black text-gray-900 flex items-center gap-2">
<Activity className="w-4 h-4 text-blue-500" />
AI Confidence: {confidence}%
</h3>
</div>
<div className="w-full h-2.5 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-1000 ${confBarColor}`}
style={{ width: `${confidence}%` }}
></div>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Collapsible: How AI reached this conclusion */}
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white overflow-hidden">
<button
onClick={() => setExplainerOpen(!explainerOpen)}
className="w-full p-6 flex items-center justify-between hover:bg-gray-50/50 transition-colors"
>
<span className="text-sm font-black text-gray-900 flex items-center gap-2">
<Lightbulb className="w-4 h-4 text-amber-500" />
How AI reached this conclusion
</span>
<ChevronDown className={`w-5 h-5 text-gray-400 transition-transform duration-300 ${explainerOpen ? 'rotate-180' : ''}`} />
</button>
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${explainerOpen ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}>
<div className="px-6 pb-6 space-y-5 border-t border-gray-100 pt-5">
{/* Signals Detected */}
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2">Signals Detected</p>
<div className="flex flex-wrap gap-2">
{signalTexts.length > 0 ? signalTexts.map((s, idx) => (
<span key={idx} className="px-3 py-1 rounded-full bg-emerald-50 border border-emerald-200 text-emerald-700 text-xs font-bold">
{s}
</span>
)) : (
<span className="text-sm text-gray-400 italic">No specific signals</span>
)}
</div>
</div>
{/* Pattern Matched */}
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2">Pattern Matched</p>
<div className="flex items-center gap-2">
<span className="px-3 py-1.5 rounded-lg bg-gray-50 border border-gray-200 text-sm font-bold text-gray-700">
{patternMatch}
</span>
</div>
</div>
{/* Confidence Explanation */}
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2">Confidence Explanation</p>
<p className="text-sm text-gray-600 font-medium leading-relaxed italic">
"{confidenceExplanation}"
</p>
</div>
</div>
</div>
</Card>
{/* 6. User Correction Section */}
<Card className="rounded-xl border border-gray-100 shadow-sm bg-white">
<CardContent className="p-8 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-black text-gray-900 flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-emerald-500" />
Is something missing?
</h3>
<button
onClick={handleUpdate}
className="text-xs font-bold text-emerald-600 hover:text-emerald-700 py-1.5 px-3 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors"
>
Update Analysis
</button>
</div>
<textarea
value={editedIssue}
onChange={(e) => setEditedIssue(e.target.value)}
className="w-full min-h-[100px] p-4 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 text-sm font-medium text-gray-700 resize-none transition-all"
placeholder="Add or correct any details here..."
/>
</CardContent>
</Card>
{/* 7. Continue Button */}
<div className="flex justify-end pt-4">
<button
onClick={handleContinue}
className="h-14 px-10 bg-emerald-500 hover:bg-emerald-600 text-white font-bold text-lg rounded-xl flex items-center justify-center gap-2 shadow-sm transition-all active:scale-[0.98]"
>
Continue
<ArrowRight className="w-5 h-5" />
</button>
</div>
</div>
</div>
);
};
export default AIUnderstanding;