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} ))}