From da8ce9147fc2a68870a86b5143ac66185d710472 Mon Sep 17 00:00:00 2001 From: Yashas Date: Thu, 10 Sep 2026 13:26:04 +0530 Subject: [PATCH] console: the WhatsApp entry says which voice is a machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Priya Raghavan and Engage" reads as two colleagues. Every other entry in the trail already answers this — the agent's disc carries a sparkle, its name carries an AI tag, a person's disc carries their initials — and the exchange entry was the one place printing bare names, on the screen whose whole claim is that the work was done autonomously. So the participants are chips rather than a sentence, and so is the label on every message, in the preview and in the thread: Priya Raghavan wears her initials, Engage wears the sparkle and the AI tag, and ops replying by hand wears their initials too — a person answering in the thread must never be dressed as the machine. The agent chip opens its card, as it does everywhere else in the trail; the label inside the preview card stays a span, because the card is already a button. The disc's tint and the bubble's tint are the same token, so a chip dropped into a bubble lost its disc; it is white inside one. `.bub span` narrowed to direct children other than the attribution row, so the chip's own spans are not restyled and CallTranscript's plain labels still work. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/AgentChip.css | 5 +++ src/components/Conversation.css | 19 ++++++++++- src/components/Conversation.jsx | 18 +++++++---- src/components/Timeline.css | 17 ++++++---- src/components/Timeline.jsx | 56 +++++++++++++++++++++++++++------ 5 files changed, 92 insertions(+), 23 deletions(-) diff --git a/src/components/AgentChip.css b/src/components/AgentChip.css index 08373cc..cccbdda 100644 --- a/src/components/AgentChip.css +++ b/src/components/AgentChip.css @@ -8,6 +8,11 @@ font-size: 11px; font-weight: 700; letter-spacing: .02em; user-select: none; background: var(--zk-tint-blue); color: var(--zk-blue); } +/* xs is the label on a chat bubble, where the disc sits beside 12.5px type. */ +.who--xs { gap: 5px; } +.who--xs .who__disc { width: 16px; height: 16px; font-size: 8.5px; } +.who--xs .who__name { font-size: var(--fs-3xs); } +.who--xs .who__tag { font-size: 10px; padding: 0 4px; } .who--md .who__disc { width: 36px; height: 36px; font-size: 13px; } .who--lg .who__disc { width: 40px; height: 40px; font-size: 14px; } diff --git a/src/components/Conversation.css b/src/components/Conversation.css index 1c757cc..407e8d6 100644 --- a/src/components/Conversation.css +++ b/src/components/Conversation.css @@ -34,13 +34,30 @@ overflow-wrap: anywhere; } -.bub span { +/* Direct children only, and not the attribution row: the chip in there brings + its own spans, and CallTranscript still labels its turns with a plain one. */ +.bub > span:not(.bub__by) { display: block; margin-top: 4px; font-size: var(--fs-2xs); opacity: 0.65; } +/* Who said it, and whether they are a person. */ +.bub__by { + display: flex; + align-items: center; + gap: 8px; + margin-top: 6px; +} +.bub__by time { + font-size: var(--fs-2xs); + color: var(--zk-grey); +} +/* The disc's tint and the bubble's tint are the same token, so a chip dropped + into a bubble loses its disc entirely. White puts it back. */ +.bub .who__disc { background: #fff; } + .bub--them { align-self: flex-start; background: var(--zk-line-soft); diff --git a/src/components/Conversation.jsx b/src/components/Conversation.jsx index 7f771c7..eef1564 100644 --- a/src/components/Conversation.jsx +++ b/src/components/Conversation.jsx @@ -1,5 +1,6 @@ import { Fragment, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' +import AgentChip from './AgentChip.jsx' import { buildThread, listOf } from '../api/thread.js' import './Conversation.css' @@ -137,12 +138,17 @@ export default function Conversation({ rows, name, anchor, onClose }) { {ep.turns.map((t) => (

{t.text}

- - {/* Named on every message, both directions. The previous - version stamped only the time, so a thread read back - months later could not say which of our replies a - person wrote and which Engage did. */} - {t.side === 'them' ? (name || 'the customer') : t.by} · {at(t.at)} + + {/* Named AND marked, both directions. It stamped only the + time, so a thread read back months later could not say + which of our replies a person wrote and which Engage + did — and a bare "Engage" reads as a colleague. The + sparkle disc and the AI tag are how the rest of this + console says "machine"; a person keeps their initials. */} + {t.side === 'them' + ? + : } +
))} diff --git a/src/components/Timeline.css b/src/components/Timeline.css index 8d687da..2035fb2 100644 --- a/src/components/Timeline.css +++ b/src/components/Timeline.css @@ -97,19 +97,25 @@ .tl__wa:hover { border-color: var(--zk-blue-light); } .tl__wa:hover .tl__wafoot { text-decoration: underline; } +/* The participants, as chips. A person's disc carries their initials, an + agent's carries the sparkle and an AI tag — which is the whole point of the + line: one of these is not a colleague. */ +.tl__wavoices { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 1px; } +.tl__wavoices .who--btn { margin-left: -2px; } + .tl__wapre { display: flex; flex-direction: column; gap: 6px; } .tl__wamsg { max-width: 88%; padding: 7px 11px 6px; border-radius: 12px; background: var(--zk-line-soft); } -.tl__wamsg em { - display: block; font-style: normal; margin-bottom: 2px; - font-size: var(--fs-3xs); font-weight: 600; color: var(--zk-grey); -} +.tl__wamsg-who { display: block; margin-bottom: 3px; } +/* Same reason as in the dialog: the disc's tint and the bubble's are one + token, so the disc vanishes into it without this. */ +.tl__wamsg .who__disc { background: #fff; } /* Two lines of each message, no more. A preview that grows with whatever somebody typed has stopped being a preview. */ -.tl__wamsg > span { +.tl__wamsg-txt { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; font-size: 14.5px; line-height: 1.45; color: var(--zk-ink); @@ -120,7 +126,6 @@ .tl__wamsg--us { align-self: flex-end; background: var(--zk-tint-blue); border-bottom-right-radius: 4px; } -.tl__wamsg--us em { color: var(--zk-blue); } .tl__wafoot { display: flex; align-items: center; gap: 4px; margin-top: 9px; diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index e55dadd..6066d01 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -184,15 +184,41 @@ function WaGlyph() { ) } -/** Who spoke in an exchange: "Priya and Engage". Named rather than counted — - * "2 participants" is a fact nobody needed. */ +/** + * Who spoke in an exchange — as CHIPS, not as a sentence. + * + * "Priya Raghavan and Engage" reads as two colleagues. On a screen whose whole + * claim is that the work was done autonomously, the one fact every line has to + * state is which of the two is a machine, and a bare name states the opposite. + * + * Every other entry in the trail already says it the same way — the agent's + * disc carries a sparkle, its name carries an AI tag, a person's disc carries + * their initials — so this is the existing convention reaching somewhere it was + * missing rather than a new one. + * + * The customer comes first: they are the reason the exchange exists. + */ function voicesOf(ep, customerName) { const who = [] - if (ep.turns.some((t) => t.side === 'them')) who.push(customerName || 'the customer') - for (const t of ep.turns) { - if (t.side === 'us' && !who.includes(t.by)) who.push(t.by) + if (ep.turns.some((t) => t.side === 'them')) { + who.push({ key: 'them', agentKey: null, name: customerName || 'the customer' }) } - return listOf(who, 3) + for (const t of ep.turns) { + if (t.side !== 'us') continue + // Keyed on the agent, or on the name for a person answering by hand: ops + // replying in the thread is a person and must never wear the sparkle. + const key = t.agentKey || `person:${t.by}` + if (!who.some((w) => w.key === key)) who.push({ key, agentKey: t.agentKey, name: t.by }) + } + return who +} + +/** The label on a preview bubble: the same chip, small, and never a button — + * the card it sits in is already one, and a button inside a button is not. */ +function TurnWho({ turn, customerName }) { + return turn.side === 'them' + ? + : } /** @@ -466,8 +492,16 @@ export default function Timeline({ rows, customerName, onOpenAgent, onOpenChat, {spanAt(it.ep.from, it.ep.to)} -
- {voicesOf(it.ep, customerName)} +
+ {voicesOf(it.ep, customerName).map((w) => ( + + ))}
{it.ep.after.length ? ( @@ -488,8 +522,10 @@ export default function Timeline({ rows, customerName, onOpenAgent, onOpenChat, {it.ep.preview.map((t) => ( - {t.side === 'them' ? (customerName || 'the customer') : t.by} - {t.text} + + + + {t.text} ))}