Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
/**
* ╔═══════════════════════════════════════════════════════════════════════════╗
* β•‘ 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;