console: the story is the page; the reference is the rail
The audit trail is the record of work nobody watched happen — it is what this product is for — and it sat in a 384px gutter with its own inner scrollbar beside a full-width panel of form fields. That said, in layout, that the AI's work was a footnote to the data entry. Exactly backwards. Swapped. The trail takes the wide column and scrolls with the page under a heading that says what it is: "What happened". The reference data — customer, vehicle, documents, the call, the stage's own facts — moves to a 340px sticky rail, where short key/value pairs belong. The crew who worked the lead sits at the top of that rail as its own small panel. The overview now opens on who holds the work rather than on money: four large numbers on navy — with the AI, waiting on a person, with the customer, renewals at risk. A live person-queue or a lapsed renewal warms its cell; the rest stay cool. Money is the outcome; this is the state, and it is the sentence the whole console exists to say. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9c05e4b26b
commit
a49ae3d86c
@ -677,6 +677,59 @@ export default function Lead() {
|
|||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
{/* THE STORY IS THE MAIN COLUMN NOW.
|
||||||
|
This sat in a 384px gutter with its own inner scrollbar, beside a
|
||||||
|
panel of reference fields that had the whole width. That was
|
||||||
|
exactly backwards. The trail is what this product is FOR — it is
|
||||||
|
the record of work nobody watched happen — and putting it in a
|
||||||
|
sidebar said it was a footnote to the form fields.
|
||||||
|
|
||||||
|
It gets the wide column and the page's own scroll. The reference
|
||||||
|
data moved to the rail, where short key/value pairs belong and
|
||||||
|
where a narrow measure costs nothing. */}
|
||||||
|
<div className="panel">
|
||||||
|
<div className="panel__head">
|
||||||
|
<div>
|
||||||
|
<h2 className="panel__title">What happened</h2>
|
||||||
|
<p className="panel__sub">
|
||||||
|
Every step on this lead, who took it, and what they wrote.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{chatTurns ? (
|
||||||
|
<button type="button" className="panel__act" onClick={() => setShowChat(true)}>
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M2.5 3.5h11v7h-6l-3 2.5v-2.5h-2z" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.3" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
Conversation
|
||||||
|
<b>{chatTurns}</b>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Timeline
|
||||||
|
rows={audit}
|
||||||
|
onOpenAgent={setOpenAgent}
|
||||||
|
onOpenChat={() => setShowChat(true)}
|
||||||
|
onOpenCall={() => setShowCall(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reference, not narrative. Sticky, so the facts stay put while the
|
||||||
|
story is read against them. */}
|
||||||
|
<aside className="lead__side">
|
||||||
|
{/* Who worked this lead. Five agents carry it and their names appear
|
||||||
|
only inside individual entries, so the team was never visible at
|
||||||
|
a glance. */}
|
||||||
|
{worked.length ? (
|
||||||
|
<div className="panel panel--crew">
|
||||||
|
<span className="crew__l">Worked by</span>
|
||||||
|
<div className="crew">
|
||||||
|
{worked.map((k) => <AgentChip key={k} agentKey={k} onOpen={setOpenAgent} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{/* WHAT MATTERS HERE, NOT EVERYTHING THERE IS.
|
{/* WHAT MATTERS HERE, NOT EVERYTHING THERE IS.
|
||||||
The whole record used to sit open on the page: fifty-seven
|
The whole record used to sit open on the page: fifty-seven
|
||||||
fields over thirteen groups, which pushed the audit trail — the
|
fields over thirteen groups, which pushed the audit trail — the
|
||||||
@ -758,50 +811,6 @@ export default function Lead() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* The story runs alongside the file rather than under it, so what was
|
|
||||||
done and what was captured can be read against each other. */}
|
|
||||||
<aside className="lead__side">
|
|
||||||
<div className="panel panel--rail">
|
|
||||||
<div className="panel__head">
|
|
||||||
<div>
|
|
||||||
<h2 className="panel__title">Audit trail</h2>
|
|
||||||
<p className="panel__sub">Every step, who took it, and what they wrote.</p>
|
|
||||||
</div>
|
|
||||||
{/* The WhatsApp exchange is spread across the trail one turn at a
|
|
||||||
time, so reading it meant scrolling a log and holding the order
|
|
||||||
in your head. This shows it as the thread it is. */}
|
|
||||||
{chatTurns ? (
|
|
||||||
<button type="button" className="panel__act" onClick={() => setShowChat(true)}>
|
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
||||||
<path d="M2.5 3.5h11v7h-6l-3 2.5v-2.5h-2z" fill="none" stroke="currentColor"
|
|
||||||
strokeWidth="1.3" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
Conversation
|
|
||||||
<b>{chatTurns}</b>
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Who worked this lead. Five agents carry it and their names appear
|
|
||||||
only inside individual entries, so the team was never visible at
|
|
||||||
a glance. */}
|
|
||||||
{worked.length ? (
|
|
||||||
<div className="crew">
|
|
||||||
<span className="crew__l">Worked by</span>
|
|
||||||
{worked.map((k) => <AgentChip key={k} agentKey={k} onOpen={setOpenAgent} />)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="lead__feed">
|
|
||||||
<Timeline
|
|
||||||
rows={audit}
|
|
||||||
onOpenAgent={setOpenAgent}
|
|
||||||
onOpenChat={() => setShowChat(true)}
|
|
||||||
onOpenCall={() => setShowCall(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -133,8 +133,24 @@ export default function Overview() {
|
|||||||
// never appears to a partner agent who could not act on it anyway.
|
// never appears to a partner agent who could not act on it anyway.
|
||||||
const approvals = actionable.filter((s) => s.approval && roles.includes(s.approval))
|
const approvals = actionable.filter((s) => s.approval && roles.includes(s.approval))
|
||||||
|
|
||||||
|
// THE HEADLINE OF AN AGENTIC CONSOLE IS WHO HOLDS THE WORK RIGHT NOW.
|
||||||
|
// Every open lead is in exactly one of three hands: a person's, the AI's,
|
||||||
|
// or the customer's. The page led with money, which is the outcome; this
|
||||||
|
// is the state, and it is the sentence the whole product exists to say.
|
||||||
|
const byStageDef = new Map(STAGES.map((s) => [s.name, s]))
|
||||||
|
let withAI = 0, withCustomer = 0, withPerson = 0
|
||||||
|
for (const r of open) {
|
||||||
|
const def = byStageDef.get(r.current_state_name)
|
||||||
|
if (!def) continue
|
||||||
|
const ph = phaseOf(def, r)
|
||||||
|
if (ph.kind === 'auto') withAI += 1
|
||||||
|
else if (ph.kind === 'customer') withCustomer += 1
|
||||||
|
else withPerson += 1
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
byStage, open, won, lost, closed, gwp, commission, pipeline, exposure, attention, actionable, approvals,
|
byStage, open, won, lost, closed, gwp, commission, pipeline, exposure, attention, actionable, approvals,
|
||||||
|
withAI, withCustomer, withPerson,
|
||||||
conversion: closed ? Math.round((won.length / closed) * 100) : null,
|
conversion: closed ? Math.round((won.length / closed) * 100) : null,
|
||||||
maxStage: Math.max(1, ...STAGES.map((s) => byStage[s.name] || 0)),
|
maxStage: Math.max(1, ...STAGES.map((s) => byStage[s.name] || 0)),
|
||||||
}
|
}
|
||||||
@ -193,6 +209,33 @@ export default function Overview() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* Who holds the work, right now. Three numbers, one sentence: this many
|
||||||
|
are being worked by the AI, this many are waiting on a person, this
|
||||||
|
many on a customer. It sits above the money because it is the claim
|
||||||
|
the money is evidence for. */}
|
||||||
|
<div className="now">
|
||||||
|
<div className="now__cell now__cell--ai">
|
||||||
|
<span className="now__l">With the AI</span>
|
||||||
|
<strong className="now__v">{m.withAI}</strong>
|
||||||
|
<span className="now__s">being qualified, rated, checked or chased — no one waiting</span>
|
||||||
|
</div>
|
||||||
|
<div className={'now__cell now__cell--person' + (m.withPerson ? ' is-live' : '')}>
|
||||||
|
<span className="now__l">Waiting on a person</span>
|
||||||
|
<strong className="now__v">{m.withPerson}</strong>
|
||||||
|
<span className="now__s">{m.withPerson ? 'a decision or an upload is owed' : 'nothing owed by anyone'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="now__cell now__cell--cust">
|
||||||
|
<span className="now__l">With the customer</span>
|
||||||
|
<strong className="now__v">{m.withCustomer}</strong>
|
||||||
|
<span className="now__s">a quote or a payment is theirs to answer</span>
|
||||||
|
</div>
|
||||||
|
<div className={'now__cell now__cell--risk' + (m.exposure.lapsed.length ? ' is-live' : '')}>
|
||||||
|
<span className="now__l">Renewals at risk</span>
|
||||||
|
<strong className="now__v">{m.exposure.lapsed.length + m.exposure.week.length}</strong>
|
||||||
|
<span className="now__s">{m.exposure.lapsed.length} lapsed · {m.exposure.week.length} within 7 days</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="kpis">
|
<div className="kpis">
|
||||||
<div className="kpi">
|
<div className="kpi">
|
||||||
<span className="kpi__l">Open leads</span>
|
<span className="kpi__l">Open leads</span>
|
||||||
|
|||||||
@ -589,11 +589,14 @@
|
|||||||
/* ---- lead: the file beside the story ---- */
|
/* ---- lead: the file beside the story ---- */
|
||||||
.lead {
|
.lead {
|
||||||
display: grid;
|
display: grid;
|
||||||
/* Wider than it was. At 384px the trail was a column of two-line entries
|
/* THE STORY GETS THE WIDE COLUMN. The trail used to sit in a 384px gutter
|
||||||
beside a full-width file, which read as a margin note rather than as the
|
with its own scrollbar while a panel of reference fields had the whole
|
||||||
other half of the page — and it is the half that says what happened. */
|
width — exactly backwards for a product whose point is the record of
|
||||||
grid-template-columns: minmax(0, 1fr) 440px;
|
work nobody watched. Now the narrative has the measure prose needs and
|
||||||
gap: 20px;
|
the reference rail is narrow, because short key/value pairs are the one
|
||||||
|
thing a narrow column is good for. */
|
||||||
|
grid-template-columns: minmax(0, 1fr) 340px;
|
||||||
|
gap: var(--sp-6);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -604,10 +607,19 @@
|
|||||||
/* The trail stays in view while the file is read against it. */
|
/* The trail stays in view while the file is read against it. */
|
||||||
.lead__side {
|
.lead__side {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 90px;
|
top: 92px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-height: calc(100vh - 108px);
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rail panels are tighter than main panels — a 340px measure cannot afford
|
||||||
|
32px of padding on each side. */
|
||||||
|
.lead__side .panel { padding: var(--sp-5) 0 0; margin-bottom: var(--sp-4); }
|
||||||
|
.lead__side .panel > .panel__head { padding: 0 var(--sp-6); margin-bottom: var(--sp-4); }
|
||||||
|
.lead__side .panel--crew { padding: var(--sp-5) var(--sp-6); }
|
||||||
|
|
||||||
.panel--rail {
|
.panel--rail {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -619,25 +631,19 @@
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lead__feed {
|
/* .lead__feed is gone: the trail is page content and scrolls with the page.
|
||||||
max-height: calc(100vh - 240px);
|
An inner scroll region is a widget; the story is not a widget. */
|
||||||
overflow-y: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
padding: 0 22px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* One column below the width where a 384px rail stops paying for itself. */
|
/* One column below the width where a rail stops paying for itself. */
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1100px) {
|
||||||
.lead {
|
.lead {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lead__side {
|
.lead__side {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
.lead__feed {
|
|
||||||
max-height: 560px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -857,6 +863,72 @@
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Right now ─────────────────────────────────────────────────────────────
|
||||||
|
The overview's first row. Four large numbers on a navy ground so the page
|
||||||
|
opens on a statement rather than a grid: this is who holds the work. Each
|
||||||
|
cell carries its own hue at the top edge — blue for the machine, amber for
|
||||||
|
a person, teal for the customer, red for risk — so the row can be read as
|
||||||
|
colour before it is read as text. */
|
||||||
|
.now {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: var(--sp-3);
|
||||||
|
margin-bottom: var(--sp-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.now__cell {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--sp-6) var(--sp-6) var(--sp-5);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
background: var(--zk-navy);
|
||||||
|
color: var(--zk-white);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--sh-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.now__cell::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto 0;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--zk-blue-light);
|
||||||
|
}
|
||||||
|
.now__cell--person::before { background: var(--zk-amber-line); }
|
||||||
|
.now__cell--cust::before { background: var(--zk-cyan); }
|
||||||
|
.now__cell--risk::before { background: var(--zk-danger-line); }
|
||||||
|
|
||||||
|
/* A live person-queue or a lapsed renewal is the one thing on this page that
|
||||||
|
is actually owed. It warms up; the rest stay navy. */
|
||||||
|
.now__cell--person.is-live { background: var(--zk-amber-ink); }
|
||||||
|
.now__cell--risk.is-live { background: var(--zk-danger-ink); }
|
||||||
|
|
||||||
|
.now__l {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--sp-3);
|
||||||
|
font-size: var(--fs-3xs);
|
||||||
|
font-weight: var(--fw-semi);
|
||||||
|
letter-spacing: .09em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: .78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now__v {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--sp-2);
|
||||||
|
font-size: var(--fs-3xl);
|
||||||
|
font-weight: var(--fw-semi);
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -.03em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now__s {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--fs-2xs);
|
||||||
|
line-height: var(--lh-snug);
|
||||||
|
opacity: .82;
|
||||||
|
}
|
||||||
|
|
||||||
/* KPI strip. Whitespace rather than rules — see .lmetrics for why the cage
|
/* KPI strip. Whitespace rather than rules — see .lmetrics for why the cage
|
||||||
went. Tabular figures so the row does not jitter as it refreshes. */
|
went. Tabular figures so the row does not jitter as it refreshes. */
|
||||||
.kpis {
|
.kpis {
|
||||||
@ -1273,18 +1345,21 @@
|
|||||||
/* ---- who worked this lead ----
|
/* ---- who worked this lead ----
|
||||||
Five agents carry a lead and their names appeared only inside individual
|
Five agents carry a lead and their names appeared only inside individual
|
||||||
entries. The team is a fact about the lead, so it sits above the trail. */
|
entries. The team is a fact about the lead, so it sits above the trail. */
|
||||||
|
.panel--crew { padding: var(--sp-5) var(--sp-6); }
|
||||||
|
|
||||||
.crew {
|
.crew {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: var(--sp-2) var(--sp-4);
|
||||||
padding: 10px 18px 12px;
|
margin-top: var(--sp-3);
|
||||||
border-bottom: 1px solid var(--zk-line-soft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.crew__l {
|
.crew__l {
|
||||||
|
display: block;
|
||||||
font-size: var(--fs-3xs);
|
font-size: var(--fs-3xs);
|
||||||
font-weight: 500;
|
font-weight: var(--fw-semi);
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.09em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--zk-grey);
|
color: var(--zk-grey);
|
||||||
}
|
}
|
||||||
@ -1541,10 +1616,9 @@
|
|||||||
row of its own the way the thirteen-group version did. */
|
row of its own the way the thirteen-group version did. */
|
||||||
.glance {
|
.glance {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
grid-template-columns: 1fr;
|
||||||
gap: 14px;
|
gap: var(--sp-3);
|
||||||
padding: 4px 22px 18px;
|
padding: 0 var(--sp-6) var(--sp-4);
|
||||||
align-items: start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.glance .props { display: block; padding: 0; }
|
.glance .props { display: block; padding: 0; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user