Spaces:
Paused
Paused
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β KNOWLEDGE GAP CARD β | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β Visual representation of knowledge gaps in the system β | |
| * β Part of the Liquid UI Arsenal β | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| */ | |
| import { useState } from 'react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { Progress } from '@/components/ui/progress'; | |
| import { cn } from '@/lib/utils'; | |
| import { | |
| HelpCircle, Clock, CheckCircle, AlertTriangle, | |
| Zap, RefreshCw, ChevronDown, ChevronUp, | |
| Target, TrendingUp, ExternalLink | |
| } from 'lucide-react'; | |
| export interface KnowledgeGap { | |
| id: string; | |
| query: string; | |
| status: 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'STALE'; | |
| lifecycle: 'ONE_OFF' | 'CONSTANT_STREAM'; | |
| priority: 'critical' | 'high' | 'medium' | 'low'; | |
| confidence?: number; | |
| created_at: string; | |
| updated_at?: string; | |
| resolution?: { | |
| knowledge: string; | |
| source: string; | |
| resolved_at: string; | |
| }; | |
| suggestedResolvers?: string[]; | |
| tags?: string[]; | |
| } | |
| export interface KnowledgeGapCardProps { | |
| gap: KnowledgeGap; | |
| onResolve?: (gapId: string) => void; | |
| onTriggerResolution?: (gapId: string) => void; | |
| compact?: boolean; | |
| } | |
| const statusConfig = { | |
| OPEN: { icon: HelpCircle, color: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/30' }, | |
| IN_PROGRESS: { icon: RefreshCw, color: 'text-yellow-400', bg: 'bg-yellow-500/10', border: 'border-yellow-500/30' }, | |
| RESOLVED: { icon: CheckCircle, color: 'text-green-400', bg: 'bg-green-500/10', border: 'border-green-500/30' }, | |
| STALE: { icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-500/10', border: 'border-red-500/30' }, | |
| }; | |
| const priorityConfig = { | |
| critical: { color: 'text-red-500', bg: 'bg-red-500/20', pulse: true }, | |
| high: { color: 'text-orange-500', bg: 'bg-orange-500/20', pulse: false }, | |
| medium: { color: 'text-yellow-500', bg: 'bg-yellow-500/20', pulse: false }, | |
| low: { color: 'text-gray-400', bg: 'bg-gray-500/20', pulse: false }, | |
| }; | |
| export function KnowledgeGapCard({ | |
| gap, | |
| onResolve, | |
| onTriggerResolution, | |
| compact = false, | |
| }: KnowledgeGapCardProps) { | |
| const [expanded, setExpanded] = useState(false); | |
| const status = statusConfig[gap.status]; | |
| const priority = priorityConfig[gap.priority]; | |
| const StatusIcon = status.icon; | |
| const timeSince = (dateStr: string) => { | |
| const date = new Date(dateStr); | |
| const now = new Date(); | |
| const diffMs = now.getTime() - date.getTime(); | |
| const diffMins = Math.floor(diffMs / 60000); | |
| const diffHours = Math.floor(diffMins / 60); | |
| const diffDays = Math.floor(diffHours / 24); | |
| if (diffDays > 0) return `${diffDays}d ago`; | |
| if (diffHours > 0) return `${diffHours}h ago`; | |
| if (diffMins > 0) return `${diffMins}m ago`; | |
| return 'just now'; | |
| }; | |
| if (compact) { | |
| return ( | |
| <div className={cn( | |
| 'flex items-center gap-3 px-3 py-2 rounded border transition-all', | |
| status.bg, status.border, 'hover:bg-opacity-20' | |
| )}> | |
| <StatusIcon className={cn('w-4 h-4 flex-shrink-0', status.color)} /> | |
| <span className="flex-1 text-sm truncate">{gap.query}</span> | |
| <Badge className={cn('text-[9px]', priority.bg, priority.color)}> | |
| {gap.priority} | |
| </Badge> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className={cn( | |
| 'rounded-lg border overflow-hidden transition-all', | |
| status.bg, status.border | |
| )}> | |
| {/* Header */} | |
| <div className="flex items-start justify-between p-4"> | |
| <div className="flex items-start gap-3 flex-1"> | |
| <div className={cn( | |
| 'p-2 rounded-lg', | |
| status.bg | |
| )}> | |
| <StatusIcon className={cn( | |
| 'w-5 h-5', | |
| status.color, | |
| gap.status === 'IN_PROGRESS' && 'animate-spin' | |
| )} /> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2 mb-1"> | |
| <Badge className={cn( | |
| 'text-[9px]', | |
| priority.bg, priority.color, | |
| priority.pulse && 'animate-pulse' | |
| )}> | |
| {gap.priority.toUpperCase()} | |
| </Badge> | |
| <Badge variant="outline" className="text-[9px]"> | |
| {gap.lifecycle === 'CONSTANT_STREAM' ? 'β STREAM' : '⬀ ONE-OFF'} | |
| </Badge> | |
| <span className="text-[10px] text-muted-foreground"> | |
| {timeSince(gap.created_at)} | |
| </span> | |
| </div> | |
| <p className="text-sm font-medium leading-snug">{gap.query}</p> | |
| </div> | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => setExpanded(!expanded)} | |
| className="h-7 w-7 p-0" | |
| > | |
| {expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />} | |
| </Button> | |
| </div> | |
| {/* Confidence bar */} | |
| {gap.confidence !== undefined && ( | |
| <div className="px-4 pb-3"> | |
| <div className="flex items-center justify-between text-[10px] text-muted-foreground mb-1"> | |
| <span>Detection Confidence</span> | |
| <span>{(gap.confidence * 100).toFixed(0)}%</span> | |
| </div> | |
| <Progress value={gap.confidence * 100} className="h-1" /> | |
| </div> | |
| )} | |
| {/* Expanded content */} | |
| {expanded && ( | |
| <div className="px-4 pb-4 space-y-3 border-t border-border/30 pt-3"> | |
| {/* Tags */} | |
| {gap.tags && gap.tags.length > 0 && ( | |
| <div className="flex flex-wrap gap-1"> | |
| {gap.tags.map(tag => ( | |
| <Badge key={tag} variant="outline" className="text-[9px]"> | |
| {tag} | |
| </Badge> | |
| ))} | |
| </div> | |
| )} | |
| {/* Suggested resolvers */} | |
| {gap.suggestedResolvers && gap.suggestedResolvers.length > 0 && ( | |
| <div> | |
| <span className="text-[10px] text-muted-foreground uppercase tracking-wider"> | |
| Suggested Resolvers | |
| </span> | |
| <div className="flex flex-wrap gap-1 mt-1"> | |
| {gap.suggestedResolvers.map(resolver => ( | |
| <Badge key={resolver} className="text-[9px] bg-primary/20"> | |
| <Target className="w-2 h-2 mr-1" /> | |
| {resolver} | |
| </Badge> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Resolution (if resolved) */} | |
| {gap.resolution && ( | |
| <div className="p-3 bg-green-500/10 rounded border border-green-500/30"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <CheckCircle className="w-3 h-3 text-green-400" /> | |
| <span className="text-[10px] text-green-400 uppercase tracking-wider"> | |
| Resolved {timeSince(gap.resolution.resolved_at)} | |
| </span> | |
| </div> | |
| <p className="text-xs text-foreground/80">{gap.resolution.knowledge}</p> | |
| <div className="flex items-center gap-1 mt-2 text-[10px] text-muted-foreground"> | |
| <ExternalLink className="w-3 h-3" /> | |
| Source: {gap.resolution.source} | |
| </div> | |
| </div> | |
| )} | |
| {/* Actions */} | |
| {gap.status !== 'RESOLVED' && ( | |
| <div className="flex gap-2"> | |
| {onTriggerResolution && ( | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => onTriggerResolution(gap.id)} | |
| className="h-7 text-xs flex-1" | |
| > | |
| <Zap className="w-3 h-3 mr-1" /> | |
| Trigger Resolution | |
| </Button> | |
| )} | |
| {onResolve && ( | |
| <Button | |
| variant="default" | |
| size="sm" | |
| onClick={() => onResolve(gap.id)} | |
| className="h-7 text-xs flex-1" | |
| > | |
| <CheckCircle className="w-3 h-3 mr-1" /> | |
| Mark Resolved | |
| </Button> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Status footer */} | |
| <div className={cn( | |
| 'px-4 py-2 flex items-center justify-between text-[10px]', | |
| 'bg-black/20 border-t border-border/30' | |
| )}> | |
| <span className="text-muted-foreground font-mono">ID: {gap.id.slice(0, 12)}...</span> | |
| <div className="flex items-center gap-1"> | |
| <Clock className="w-3 h-3 text-muted-foreground" /> | |
| <span className={status.color}>{gap.status}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default KnowledgeGapCard; | |