import { Link } from 'react-router-dom'; import React, { useEffect, useState } from 'react'; import type { IconType } from 'react-icons'; import { FaArrowRight, FaBoxOpen, FaCheck, FaExclamationCircle, FaExclamationTriangle, FaLifeRing, FaRegEyeSlash, FaRegQuestionCircle, FaShieldAlt, FaSpinner, } from 'react-icons/fa'; import { getDateRangeFromPreset } from '../agent-analytics/helpers'; import JourneyDetailModal from '../agent-analytics/JourneyDetailModal'; import type { AgentGapCategory, AgentGapMutation, IAgentGap, } from '../../service/agent-analytics/agent-analytics.interface'; import { fetchAgentGaps, updateAgentGapState, } from '../../service/agent-analytics/agent-analytics.service'; const GAPS_TO = '/agent-analytics?tab=gaps'; const MAX_VISIBLE = 5; interface CategoryMeta { label: string; icon: IconType; iconBg: string; iconColor: string; } const CATEGORY_META: Record = { info_missing: { label: 'Info missing', icon: FaRegQuestionCircle, iconBg: 'bg-amber-50', iconColor: 'text-amber-600', }, feature_missing: { label: 'Feature / product missing', icon: FaBoxOpen, iconBg: 'bg-rose-50', iconColor: 'text-rose-600', }, cannot_guide: { label: 'Cannot guide', icon: FaExclamationTriangle, iconBg: 'bg-orange-50', iconColor: 'text-orange-600', }, out_of_scope: { label: 'Out of scope', icon: FaLifeRing, iconBg: 'bg-sky-50', iconColor: 'text-sky-600', }, }; const categoryMetaFor = (category: AgentGapCategory): CategoryMeta => CATEGORY_META[category] ?? CATEGORY_META.info_missing; /** * Dashboard "Conversation gaps" widget — the most frequent active gaps from * the last 30 days with quick mark-fixed / dismiss actions and a conversation * drill-down. The full fix flow lives on the Agent Analytics Gaps tab. * * @param {object} props - Recomaze auth. * @return {React.ReactElement} The gaps section. */ export function LatestGaps({ token }: { token: string }) { const [gaps, setGaps] = useState([]); const [hasMore, setHasMore] = useState(false); const [loading, setLoading] = useState(true); const [pendingKey, setPendingKey] = useState(null); const [openFingerprint, setOpenFingerprint] = useState(null); useEffect(() => { if (!token) return; let cancelled = false; const range = getDateRangeFromPreset('30d'); setLoading(true); void (async () => { try { const result = await fetchAgentGaps( range.start, range.end, MAX_VISIBLE, null, null, 'active' ); if (cancelled) return; setGaps(result.gaps); setHasMore( Boolean(result.next_cursor) || result.total_all_categories > result.gaps.length ); } catch { // Benign: the store may have no agent gaps yet. } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [token]); const handleMutate = async (gap: IAgentGap, status: AgentGapMutation) => { const key = `${gap.category}::${gap.target}`; setPendingKey(key); setGaps(previous => previous.filter(entry => `${entry.category}::${entry.target}` !== key) ); try { await updateAgentGapState({ target: gap.target, category: gap.category, status, }); } catch { console.warn('[Dashboard] gap mutate failed'); } finally { setPendingKey(null); } }; return (

Conversation gaps

Where your agent came up short · last 30 days

View all
{loading ? ( ) : gaps.length === 0 ? ( ) : (
{gaps.map((gap, index) => { const meta = categoryMetaFor(gap.category); const Icon = meta.icon; const key = `${gap.category}::${gap.target}`; const isPending = pendingKey === key; const firstSample = gap.samples?.[0]; return (

{gap.target}

{meta.label}

{firstSample ? ( ) : ( {gap.count} conversation{gap.count === 1 ? '' : 's'} )}
); })} {hasMore && ( More gaps to review )}
)} {openFingerprint && ( setOpenFingerprint(null)} /> )}
); } function SkeletonRows() { return (
{Array.from({ length: 3 }).map((_, index) => (
))}
); } function EmptyState() { return (

No gaps detected

Your agent is handling questions without falling short. New gaps surface here as they happen.

); }