import { useEffect, useState } from 'react' import { useZino } from '../api/provider.jsx' import ClampText from './ClampText.jsx' import { STAGES } from '../api/config.js' import './Timeline.css' /** Which employee holds which role — used to badge an entry as machine work. */ const AI_ROLES = { ai_intake: 'Intake & Attribution', ai_engage: 'Engage', ai_kyc: 'KYC & Evidence', ai_advisor: 'Advisor', ai_uw_referral: 'Underwriting Referral', } /** * The fields worth surfacing per activity — an agent's reasoning, a rule's * output, a call's notes. Everything else stays in the file below; a timeline * that shows every field is a table, not a story. */ const NARRATIVE = [ ['attribution_reason', 'Attribution'], ['eligibility_reason', 'Eligibility'], ['dedupe_match_ref', 'Duplicate of'], ['contact_notes', 'Call'], ['ai_recommendation_rationale', 'Recommendation'], ['quoted_breakup', 'How the premium was reached'], ['kyc_mismatch_notes', 'KYC'], ['referral_analysis', 'Referral analysis'], ['uw_decision_notes', 'Underwriting decision'], ['documents_notes', 'Documents'], ] const MONEY = new Set(['quoted_premium', 'commission_amount', 'sme_value_at_risk', 'motor_idv']) function when(ts) { if (!ts) return '' const d = new Date(ts) const mins = Math.round((Date.now() - d.getTime()) / 60000) const rel = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.round(mins / 60)}h ago` : `${Math.round(mins / 1440)}d ago` return `${d.toLocaleString('en-IN', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })} · ${rel}` } export default function Timeline({ instanceId }) { const { client } = useZino() const [rows, setRows] = useState(null) const [err, setErr] = useState(null) // DATA_UPDATE entries are the platform writing fields, not anyone deciding // anything. They are the bulk of a busy trail and they are hidden until asked // for — the story is what people and agents did. const [showSys, setShowSys] = useState(false) useEffect(() => { let dead = false client.audit(instanceId) .then((r) => { if (!dead) setRows(Array.isArray(r) ? r : (r?.data ?? [])) }) .catch((e) => { if (!dead) setErr(e) }) return () => { dead = true } }, [client, instanceId]) if (err) return
Could not load the timeline — {err.status} {err.message}
if (!rows) return

Loading the timeline…

if (!rows.length) return

Nothing has happened yet.

// Oldest first: a timeline reads forwards. const ordered = [...rows].sort((a, b) => String(a.created_at).localeCompare(String(b.created_at))) const items = ordered.map((r, i) => { const roles = r.user_roles || [] const aiRole = roles.find((x) => AI_ROLES[x]) const isSystem = r.activity_id === 'DATA_UPDATE' const kind = aiRole ? 'ai' : isSystem ? 'sys' : 'human' const d = r.data || {} return { key: r.id ?? i, kind, actor: aiRole ? AI_ROLES[aiRole] : (r.user_name || 'System'), what: isSystem ? 'Data updated' : (r.activity_name || r.activity_id), stage: STAGES.find((s) => s.uid === r.execution_state), when: when(r.created_at), narrative: NARRATIVE .map(([k, label]) => [label, d[k]]) .filter(([, v]) => v !== undefined && v !== null && String(v).trim() !== ''), figures: Object.entries(d) .filter(([k, v]) => MONEY.has(k) && v) .map(([k, v]) => [k.replace(/_/g, ' '), '₹' + Number(v).toLocaleString('en-IN')]), } }) const sysCount = items.filter((it) => it.kind === 'sys').length const visible = showSys ? items : items.filter((it) => it.kind !== 'sys') return ( <> {sysCount ? ( ) : null}
    {visible.map((it) => (
  1. ))}
) }