import { useEffect, useRef } from 'react' import { baseFieldId } from '../api/config.js' import './Conversation.css' /** * The WhatsApp thread, read out of the audit trail. * * The trail shows each turn as its own entry among twenty others, so reading a * conversation meant scrolling a log and holding the order in your head. Here * it is what it is: a thread, oldest first, theirs on the left and ours on the * right. * * WHAT IT CAN AND CANNOT SHOW, said plainly at the foot of the panel rather * than left for someone to discover. The console can only show what the * workflow RECORDS, so anything sent from inside a trigger node and never * written to a field is invisible here however certainly the customer received * it. The acceptance confirmation used to be exactly that — the panel showed * nothing after "I confirm", which read as though we had ignored somebody * agreeing to buy — so 90 writes it into customer_answer and it appears. * * One send remains absent: the quote, which is a registered WhatsApp template * rather than text we compose. The "I have your message" receipt used to be a * second — it is gone from the workflow now (97), because it put a line saying * only "an answer is coming" between every question and its answer. * * A thread that quietly omitted any of this would be worse than one that says * which parts it holds. */ function turnsFrom(rows) { if (!Array.isArray(rows)) return [] const out = [] // Sorted on (timestamp, id). One submission writes three rows in the same // second — the trigger commit, its repeat, and the settle — so a timestamp // alone leaves their order to the sort's stability and puts a reply before // the message it answers whenever the two land in the same second. const ordered = [...rows].sort((a, b) => { const t = String(a.created_at).localeCompare(String(b.created_at)) return t !== 0 ? t : (Number(a.id) || 0) - (Number(b.id) || 0) }) for (const r of ordered) { const fields = Array.isArray(r.fields) && r.fields.length ? r.fields : Object.entries(r.data || {}).map(([k, v]) => ({ field_id: k, value: v })) for (const f of fields) { const base = baseFieldId(f.field_id) const v = typeof f.value === 'string' ? f.value.trim() : '' if (!v) continue if (base === 'customer_reply') out.push({ side: 'them', text: v, at: r.created_at, key: r.id + '-in' }) if (base === 'customer_answer') out.push({ side: 'us', text: v, at: r.created_at, key: r.id + '-out' }) } } // One submission is recorded three times — the trigger commit, its repeat, // and the settle — so the same sentence would print three times. // // De-duped against the PREVIOUS turn only, not against the whole thread. A // customer who asks the same thing twice because the first went unanswered // has said it twice, and a thread that silently showed it once would hide // exactly the impatience an operator needs to see. Only an immediate repeat // of the same side and the same words is the platform talking to itself. return out.filter((t, i) => { const prev = out[i - 1] return !(prev && prev.side === t.side && prev.text === t.text) }) } function at(ts) { const d = new Date(ts) if (isNaN(d)) return '' return d.toLocaleString('en-IN', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) } export default function Conversation({ rows, name, onClose }) { const ref = useRef(null) const restore = useRef(null) const turns = turnsFrom(rows) useEffect(() => { restore.current = document.activeElement ref.current?.focus() const onKey = (e) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', onKey) const prev = document.body.style.overflow document.body.style.overflow = 'hidden' return () => { document.removeEventListener('keydown', onKey) document.body.style.overflow = prev if (restore.current instanceof HTMLElement) restore.current.focus() } }, [onClose]) return (
e.stopPropagation()} >

WhatsApp with {name || 'the customer'}

{turns.length ? turns.map((t) => (

{t.text}

{at(t.at)}
)) : (

Nothing has been exchanged on WhatsApp yet.

)}

Shows the customer's messages and everything written back to them, including the acceptance confirmation. One automatic send is not here: the quote, which goes out as a registered WhatsApp template rather than text we compose.

) }