Compare commits
No commits in common. "900f34d11e7efb9a78a46ab01fcefd7696689098" and "a49ae3d86cca6d6f1f503e8d7c8abdbbea97332b" have entirely different histories.
900f34d11e
...
a49ae3d86c
@ -1,59 +0,0 @@
|
||||
.cot { margin-top: var(--sp-2); }
|
||||
|
||||
/* The finding, always visible — the one sentence that answers "what did it
|
||||
conclude" without opening anything. */
|
||||
.cot__finding {
|
||||
margin: 0 0 var(--sp-2);
|
||||
font-size: var(--fs-sm);
|
||||
line-height: var(--lh-normal);
|
||||
font-weight: var(--fw-medium);
|
||||
color: var(--zk-ink);
|
||||
}
|
||||
|
||||
.cot__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-1) 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: var(--fs-2xs);
|
||||
font-weight: var(--fw-semi);
|
||||
color: var(--zk-blue-dark);
|
||||
}
|
||||
.cot__brain { width: 15px; height: 15px; flex: none; }
|
||||
.cot__ct { color: var(--zk-grey); font-weight: var(--fw-normal); }
|
||||
.cot__caret { width: 11px; height: 11px; transition: transform .18s var(--ease); }
|
||||
.cot__caret.is-open { transform: rotate(90deg); }
|
||||
|
||||
.cot__steps {
|
||||
list-style: none;
|
||||
margin: var(--sp-3) 0 0;
|
||||
padding: var(--sp-3) 0 var(--sp-2);
|
||||
border-left: 2px solid var(--zk-blue-light);
|
||||
padding-left: var(--sp-4);
|
||||
}
|
||||
|
||||
.cot__step { position: relative; display: grid; grid-template-columns: 82px 1fr; gap: var(--sp-3); padding: var(--sp-2) 0; }
|
||||
.cot__step::before {
|
||||
content: ""; position: absolute; left: calc(var(--sp-4) * -1 - 6px); top: 9px;
|
||||
width: 9px; height: 9px; border-radius: 50%; background: var(--zk-white); border: 2px solid var(--zk-blue);
|
||||
}
|
||||
|
||||
.cot__mark {
|
||||
font-size: var(--fs-3xs);
|
||||
font-weight: var(--fw-semi);
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--zk-blue-dark);
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.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__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); }
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import './ChainOfThought.css'
|
||||
|
||||
/**
|
||||
* How an AI employee reached a decision, step by step.
|
||||
*
|
||||
* 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 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'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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
function findingOf(text) {
|
||||
const m = String(text).match(/^(.{24,200}?[.!?])(\s|$)/)
|
||||
return m ? m[1].trim() : null
|
||||
}
|
||||
|
||||
export default function ChainOfThought({ reasoning, decided, confidence }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const primary = reasoning.length ? reasoning[0][1] : ''
|
||||
const finding = findingOf(primary)
|
||||
|
||||
const steps = reasoning.length + 1 // each reasoning rung, plus Decided
|
||||
|
||||
return (
|
||||
<div className="cot">
|
||||
{finding ? <p className="cot__finding">{finding}</p> : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="cot__toggle"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
<svg className="cot__brain" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M6 2.5a2 2 0 0 0-2 2 2 2 0 0 0-1 3.7A2 2 0 0 0 4 11.5a2 2 0 0 0 2 2M10 2.5a2 2 0 0 1 2 2 2 2 0 0 1 1 3.7 2 2 0 0 1-1 3.3 2 2 0 0 1-2 2M8 3v10"
|
||||
fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
Chain of thought
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<ol className="cot__steps">
|
||||
{reasoning.map(([label, text], i) => (
|
||||
<li className="cot__step" key={label}>
|
||||
<span className="cot__mark">{i === 0 ? 'Reasoned' : label}</span>
|
||||
<div className="cot__b">{text}</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className="cot__step">
|
||||
<span className="cot__mark">Decided</span>
|
||||
<div className="cot__b">
|
||||
<b>{decided.what}</b>
|
||||
{decided.stage ? <> — moved to <b>{decided.stage}</b></> : null}
|
||||
{confidence != null ? (
|
||||
<div className="cot__foot">
|
||||
<span className="cot__conf">
|
||||
{Math.round(confidence * (confidence <= 1 ? 100 : 1))}% confidence
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -2,7 +2,6 @@ import { useState } from 'react'
|
||||
import { useZino } from '../api/provider.jsx'
|
||||
import AgentChip from './AgentChip.jsx'
|
||||
import ClampText from './ClampText.jsx'
|
||||
import ChainOfThought from './ChainOfThought.jsx'
|
||||
import { AGENTS } from '../api/agents.js'
|
||||
import { APP_ID, STAGES, baseFieldId } from '../api/config.js'
|
||||
import './Timeline.css'
|
||||
@ -24,34 +23,23 @@ import './Timeline.css'
|
||||
* that did appear was an accident (a DATA_UPDATE row happens to write the
|
||||
* unsuffixed key).
|
||||
*/
|
||||
/* AN AI STEP'S REASONING, in the order it is worth reading. These become the
|
||||
"Reasoned" rung of the chain of thought — the employee's own words for why
|
||||
it did what it did — rather than loose quote blocks. The first present one
|
||||
is the finding shown without opening the chain. */
|
||||
const REASONING = [
|
||||
const NARRATIVE = [
|
||||
['attribution_reason', 'Attribution'],
|
||||
['eligibility_reason', 'Eligibility'],
|
||||
['ai_recommendation_rationale', 'Cover advice'],
|
||||
['dedupe_match_ref', 'Duplicate of'],
|
||||
['contact_notes', 'Call'],
|
||||
['ai_recommendation_rationale', 'Recommendation'],
|
||||
['quoted_breakup', 'How the premium was reached'],
|
||||
['kyc_mismatch_notes', 'KYC'],
|
||||
['referral_analysis', 'Referral analysis'],
|
||||
['uw_decision_notes', 'Underwriting'],
|
||||
['contact_notes', 'Call'],
|
||||
['uw_decision_notes', 'Underwriting decision'],
|
||||
['documents_notes', 'Documents'],
|
||||
['quoted_breakup', 'How the premium was reached'],
|
||||
['customer_reply', 'The customer said'],
|
||||
['customer_answer', 'We replied'],
|
||||
['lost_reason', 'Why it was dropped'],
|
||||
['resume_note', 'Why now'],
|
||||
]
|
||||
|
||||
/* CONVERSATION lines stay as plain quotes — they are what was said to and by
|
||||
the customer, not the machine's reasoning, and belong beside the thread. */
|
||||
const CONVERSATION = [
|
||||
['customer_reply', 'The customer said'],
|
||||
['customer_answer', 'We replied'],
|
||||
['dedupe_match_ref', 'Duplicate of'],
|
||||
]
|
||||
|
||||
const NARRATIVE = [...REASONING, ...CONVERSATION]
|
||||
|
||||
const MONEY = new Set(['quoted_premium', 'commission_amount', 'sme_value_at_risk', 'motor_idv'])
|
||||
|
||||
/**
|
||||
@ -273,22 +261,9 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat, onOpenCall })
|
||||
stage: moved ? stage : null,
|
||||
when: when(r.created_at),
|
||||
docs,
|
||||
// The reasoning rungs of the chain of thought — the employee's own text.
|
||||
reasoning: REASONING
|
||||
narrative: NARRATIVE
|
||||
.map(([k, label]) => [label, byBase.get(k)?.value])
|
||||
.filter(([, v]) => v !== undefined && v !== null && typeof v !== 'object' && String(v).trim() !== ''),
|
||||
// Conversation lines stay as plain quotes.
|
||||
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, 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
|
||||
})(),
|
||||
figures: [...byBase]
|
||||
.filter(([base, f]) => MONEY.has(base) && f.value)
|
||||
.map(([base, f]) => [base.replace(/_/g, ' '), '₹' + Number(f.value).toLocaleString('en-IN')]),
|
||||
@ -340,7 +315,7 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat, onOpenCall })
|
||||
last.when = it.when
|
||||
// Keep the newest line as the preview — an operator scanning the trail
|
||||
// wants where the conversation GOT to, not where it started.
|
||||
if (it.conversation.length) last.conversation = it.conversation
|
||||
if (it.narrative.length) last.narrative = it.narrative
|
||||
last.docs = last.docs.concat(it.docs)
|
||||
continue
|
||||
}
|
||||
@ -432,33 +407,7 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat, onOpenCall })
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* THE CHAIN OF THOUGHT. For an AI step, the reasoning is not a
|
||||
loose quote — it is how the employee reached the decision, so
|
||||
it renders as the chain: what it checked, what it reasoned,
|
||||
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 ? (
|
||||
<ChainOfThought
|
||||
reasoning={it.reasoning}
|
||||
confidence={it.confidence}
|
||||
decided={{ what: it.what, stage: it.stage ? it.stage.name : null }}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* A human step's reason, and the AI step's reasoning when it is
|
||||
not the story's actor (rare), stay as a plain quote. */}
|
||||
{it.kind !== 'ai'
|
||||
? it.reasoning.map(([label, v]) => (
|
||||
<div className="tl__say" key={label}>
|
||||
<span className="tl__saylabel">{label}</span>
|
||||
<ClampText text={String(v)} title={`${label} — ${it.what}`} lines={2} threshold={120} />
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
|
||||
{/* What was said to and by the customer — always a plain quote. */}
|
||||
{it.conversation.map(([label, v]) => (
|
||||
{it.narrative.map(([label, v]) => (
|
||||
<div className="tl__say" key={label}>
|
||||
<span className="tl__saylabel">{label}</span>
|
||||
<ClampText text={String(v)} title={`${label} — ${it.what}`} lines={2} threshold={120} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user