console: chain of thought, from data the console already has
Follows the previous commit but drops everything that would have needed a backend change. The chain is built entirely from the audit trail the console already fetches: the REASONED rung is the employee's own reasoning fields (attribution_reason, ai_recommendation_rationale, and the rest — already in the data), and the DECIDED rung is the activity it committed, the state it moved the lead to, and its confidence (ai_recommendation_confidence, also a real field on the step). The finding stays visible above the chain. No dependency on any endpoint change: the tool-call rung and the tbl_ai_decisions join are gone. What remains is real, self-contained, and ships with a push. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8e86fa7a0a
commit
900f34d11e
@ -54,16 +54,6 @@
|
||||
.cot__b { font-size: var(--fs-xs); line-height: var(--lh-normal); color: var(--zk-muted); overflow-wrap: anywhere; }
|
||||
.cot__b b { color: var(--zk-ink); font-weight: var(--fw-semi); }
|
||||
|
||||
.cot__tool {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
margin: 3px 6px 0 0; padding: 2px 9px;
|
||||
border-radius: var(--r-pill); background: var(--zk-white); border: 1px solid var(--zk-line);
|
||||
font-family: var(--font-mono); font-size: 11.5px; color: var(--zk-muted);
|
||||
}
|
||||
.cot__tool::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--zk-good, #1f9d63); }
|
||||
.cot__tool.is-empty { color: var(--zk-grey); }
|
||||
.cot__tool.is-empty::before { background: var(--zk-grey); }
|
||||
|
||||
.cot__foot { display: flex; flex-wrap: wrap; gap: 6px; margin-top: var(--sp-2); }
|
||||
.cot__conf { font-size: 11.5px; font-weight: var(--fw-semi); color: var(--zk-blue-dark); background: var(--zk-tint-blue); border: 1px solid var(--zk-blue-light); padding: 1px 9px; border-radius: var(--r-pill); }
|
||||
.cot__model { font-family: var(--font-mono); font-size: 11px; color: var(--zk-grey); padding: 2px 0; }
|
||||
|
||||
|
||||
@ -6,77 +6,34 @@ import './ChainOfThought.css'
|
||||
*
|
||||
* This is the trust surface. A person signing off on — or overriding — a
|
||||
* machine's decision needs the answer to one question first: WHY did it do
|
||||
* that. Before this, the reasoning existed (the employees write it, and the
|
||||
* platform records the tools they called in tbl_ai_decisions) but the console
|
||||
* showed only the conclusion. An operator either trusted it blind or opened
|
||||
* the raw record. Neither is oversight.
|
||||
* that. Before this, the reasoning existed (the employees write it as workflow
|
||||
* fields, so it is already in the audit trail) but the console showed only the
|
||||
* conclusion. An operator either trusted it blind or went digging. Neither is
|
||||
* oversight.
|
||||
*
|
||||
* The chain reads the way the employee actually ran: what it CHECKED (the
|
||||
* tools it called, each marked for whether it came back with anything), what
|
||||
* it REASONED, and what it DECIDED. It is assembled from real fields, never
|
||||
* narrated after the fact — the reasoning is the employee's own text, the
|
||||
* tools are the calls it actually made, the decision is the activity it
|
||||
* committed and the state it moved the lead to.
|
||||
* The chain reads the way the employee's own record reads: what it REASONED and
|
||||
* what it DECIDED. It is assembled from real fields already on the step — the
|
||||
* employee's own words for the reasoning, the activity it committed and the
|
||||
* state it moved the lead to for the decision — never narrated after the fact.
|
||||
* A step may carry more than one reasoning field (attribution AND eligibility,
|
||||
* say); each is its own rung.
|
||||
*
|
||||
* A tool that returned nothing is drawn with a hollow marker. That is not
|
||||
* cosmetic: a licence check that came back empty and was read as "unlicensed"
|
||||
* is exactly how a real lead was wrongly dropped, and an operator scanning the
|
||||
* chain should see the empty result the machine reasoned from.
|
||||
* The finding — the first sentence, since the employees write the conclusion
|
||||
* first — is lifted out and stays visible, so the "why" can be read without
|
||||
* opening the chain.
|
||||
*/
|
||||
|
||||
/** The finding is the first sentence — the employees write the conclusion
|
||||
* first — lifted out so it can be read without opening the chain. */
|
||||
function findingOf(text) {
|
||||
const m = String(text).match(/^(.{24,200}?[.!?])(\s|$)/)
|
||||
return m ? m[1].trim() : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool ids are how the runtime names a call, not how a person reads one.
|
||||
* `retrieve_kb_7f9f…` is a knowledge lookup; `call_agentic_tool_29601` is a
|
||||
* registered tool by number. Give each a name an operator recognises; fall
|
||||
* back to a de-slugged version of whatever it is.
|
||||
*/
|
||||
function toolLabel(name) {
|
||||
const n = String(name)
|
||||
if (n.startsWith('retrieve_kb')) return 'Knowledge base'
|
||||
if (n.startsWith('search_knowledge')) return 'Knowledge base'
|
||||
if (n === 'get_task_context') return 'Task context'
|
||||
if (n === 'lookup_partner') return 'Partner registry'
|
||||
if (n.startsWith('call_agentic_tool')) return 'Agentic tool'
|
||||
return n
|
||||
.replace(/_[0-9a-f-]{8,}$/i, '')
|
||||
.replace(/_\d+$/, '')
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/^\w/, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
/** One pill per distinct tool. The same knowledge base queried three times is
|
||||
* one thing checked, not three; an operator wants the surfaces consulted, not
|
||||
* the call count. A tool is "empty" only if EVERY call to it came back empty. */
|
||||
function distinctTools(tools) {
|
||||
const seen = new Map()
|
||||
for (const t of tools) {
|
||||
const label = toolLabel(t.name)
|
||||
if (!seen.has(label)) seen.set(label, { name: label, empty: t.empty })
|
||||
else if (!t.empty) seen.get(label).empty = false
|
||||
}
|
||||
return [...seen.values()]
|
||||
}
|
||||
|
||||
export default function ChainOfThought({ reasoning, tools, decided, confidence, model }) {
|
||||
export default function ChainOfThought({ reasoning, decided, confidence }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const toolPills = tools && tools.length ? distinctTools(tools) : []
|
||||
const primary = reasoning.length ? reasoning[0][1] : ''
|
||||
const finding = findingOf(primary)
|
||||
|
||||
// Only the steps that actually have something behind them. A step with no
|
||||
// evidence is not drawn — a hollow "Checked" with no tools would be noise.
|
||||
const steps = []
|
||||
if (toolPills.length) steps.push('checked')
|
||||
if (reasoning.length) steps.push('reasoned')
|
||||
steps.push('decided')
|
||||
const steps = reasoning.length + 1 // each reasoning rung, plus Decided
|
||||
|
||||
return (
|
||||
<div className="cot">
|
||||
@ -93,7 +50,7 @@ export default function ChainOfThought({ reasoning, tools, decided, confidence,
|
||||
fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
Chain of thought
|
||||
<span className="cot__ct">· {steps.length} step{steps.length === 1 ? '' : 's'}</span>
|
||||
<span className="cot__ct">· {steps} step{steps === 1 ? '' : 's'}</span>
|
||||
<svg className={'cot__caret' + (open ? ' is-open' : '')} viewBox="0 0 12 12" aria-hidden="true">
|
||||
<path d="M4 2.5 8 6l-4 3.5" fill="none" stroke="currentColor" strokeWidth="1.6"
|
||||
strokeLinecap="round" strokeLinejoin="round" />
|
||||
@ -102,19 +59,6 @@ export default function ChainOfThought({ reasoning, tools, decided, confidence,
|
||||
|
||||
{open ? (
|
||||
<ol className="cot__steps">
|
||||
{toolPills.length ? (
|
||||
<li className="cot__step">
|
||||
<span className="cot__mark">Checked</span>
|
||||
<div className="cot__b">
|
||||
{toolPills.map((t, i) => (
|
||||
<span key={i} className={'cot__tool' + (t.empty ? ' is-empty' : '')} title={t.empty ? 'returned nothing' : 'returned a result'}>
|
||||
{t.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
|
||||
{reasoning.map(([label, text], i) => (
|
||||
<li className="cot__step" key={label}>
|
||||
<span className="cot__mark">{i === 0 ? 'Reasoned' : label}</span>
|
||||
@ -127,10 +71,11 @@ export default function ChainOfThought({ reasoning, tools, decided, confidence,
|
||||
<div className="cot__b">
|
||||
<b>{decided.what}</b>
|
||||
{decided.stage ? <> — moved to <b>{decided.stage}</b></> : null}
|
||||
{(confidence != null || model) ? (
|
||||
{confidence != null ? (
|
||||
<div className="cot__foot">
|
||||
{confidence != null ? <span className="cot__conf">{Math.round(confidence * (confidence <= 1 ? 100 : 1))}% confidence</span> : null}
|
||||
{model ? <span className="cot__model">{model}</span> : null}
|
||||
<span className="cot__conf">
|
||||
{Math.round(confidence * (confidence <= 1 ? 100 : 1))}% confidence
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@ -281,17 +281,14 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat, onOpenCall })
|
||||
conversation: CONVERSATION
|
||||
.map(([k, label]) => [label, byBase.get(k)?.value])
|
||||
.filter(([, v]) => v !== undefined && v !== null && typeof v !== 'object' && String(v).trim() !== ''),
|
||||
// The decision's confidence and model, when the step recorded them. The
|
||||
// confidence field is written on the advice step; ai_tools/ai_model come
|
||||
// from the audit row once view-service joins tbl_ai_decisions (until then
|
||||
// they are absent and the chain simply shows two rungs instead of three).
|
||||
// The decision's confidence, when the step recorded it — ai_recommendation_confidence
|
||||
// is a real field the advice step writes into the audit data, so it needs
|
||||
// no backend change to reach here.
|
||||
confidence: (() => {
|
||||
const c = byBase.get('ai_recommendation_confidence')?.value
|
||||
const n = c == null ? null : Number(c)
|
||||
return Number.isFinite(n) ? n : null
|
||||
})(),
|
||||
tools: Array.isArray(r.ai_tools) ? r.ai_tools : [],
|
||||
model: r.ai_model || '',
|
||||
figures: [...byBase]
|
||||
.filter(([base, f]) => MONEY.has(base) && f.value)
|
||||
.map(([base, f]) => [base.replace(/_/g, ' '), '₹' + Number(f.value).toLocaleString('en-IN')]),
|
||||
@ -441,12 +438,10 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat, onOpenCall })
|
||||
what it decided. This is the answer to "why did it do that",
|
||||
which is the first thing a person needs before trusting or
|
||||
overriding a machine. */}
|
||||
{it.kind === 'ai' && (it.reasoning.length || it.tools.length) ? (
|
||||
{it.kind === 'ai' && it.reasoning.length ? (
|
||||
<ChainOfThought
|
||||
reasoning={it.reasoning}
|
||||
tools={it.tools}
|
||||
confidence={it.confidence}
|
||||
model={it.model}
|
||||
decided={{ what: it.what, stage: it.stage ? it.stage.name : null }}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user