console: the WhatsApp entry says which voice is a machine
"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) <noreply@anthropic.com>
This commit is contained in:
parent
708cf4bc40
commit
da8ce9147f
@ -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; }
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) => (
|
||||
<div key={t.key} className={'bub bub--' + t.side}>
|
||||
<p>{t.text}</p>
|
||||
<span>
|
||||
{/* 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)}
|
||||
<span className="bub__by">
|
||||
{/* 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'
|
||||
? <AgentChip name={name || 'the customer'} size="xs" />
|
||||
: <AgentChip agentKey={t.agentKey} name={t.by} size="xs" />}
|
||||
<time dateTime={t.at}>{at(t.at)}</time>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'
|
||||
? <AgentChip name={customerName || 'the customer'} size="xs" />
|
||||
: <AgentChip agentKey={turn.agentKey} name={turn.by} size="xs" />
|
||||
}
|
||||
|
||||
/**
|
||||
@ -466,8 +492,16 @@ export default function Timeline({ rows, customerName, onOpenAgent, onOpenChat,
|
||||
</span>
|
||||
<span className="tl__when">{spanAt(it.ep.from, it.ep.to)}</span>
|
||||
</div>
|
||||
<div className="tl__by">
|
||||
<span className="tl__byname">{voicesOf(it.ep, customerName)}</span>
|
||||
<div className="tl__wavoices">
|
||||
{voicesOf(it.ep, customerName).map((w) => (
|
||||
<AgentChip
|
||||
key={w.key}
|
||||
agentKey={w.agentKey}
|
||||
name={w.name}
|
||||
size="sm"
|
||||
onOpen={w.agentKey ? onOpenAgent : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{it.ep.after.length ? (
|
||||
@ -488,8 +522,10 @@ export default function Timeline({ rows, customerName, onOpenAgent, onOpenChat,
|
||||
<span className="tl__wapre">
|
||||
{it.ep.preview.map((t) => (
|
||||
<span key={t.key} className={'tl__wamsg tl__wamsg--' + t.side}>
|
||||
<em>{t.side === 'them' ? (customerName || 'the customer') : t.by}</em>
|
||||
<span>{t.text}</span>
|
||||
<span className="tl__wamsg-who">
|
||||
<TurnWho turn={t} customerName={customerName} />
|
||||
</span>
|
||||
<span className="tl__wamsg-txt">{t.text}</span>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user