Compare commits

..

No commits in common. "900f34d11e7efb9a78a46ab01fcefd7696689098" and "a49ae3d86cca6d6f1f503e8d7c8abdbbea97332b" have entirely different histories.

3 changed files with 11 additions and 208 deletions

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import { useState } from 'react'
import { useZino } from '../api/provider.jsx' import { useZino } from '../api/provider.jsx'
import AgentChip from './AgentChip.jsx' import AgentChip from './AgentChip.jsx'
import ClampText from './ClampText.jsx' import ClampText from './ClampText.jsx'
import ChainOfThought from './ChainOfThought.jsx'
import { AGENTS } from '../api/agents.js' import { AGENTS } from '../api/agents.js'
import { APP_ID, STAGES, baseFieldId } from '../api/config.js' import { APP_ID, STAGES, baseFieldId } from '../api/config.js'
import './Timeline.css' import './Timeline.css'
@ -24,34 +23,23 @@ import './Timeline.css'
* that did appear was an accident (a DATA_UPDATE row happens to write the * that did appear was an accident (a DATA_UPDATE row happens to write the
* unsuffixed key). * unsuffixed key).
*/ */
/* AN AI STEP'S REASONING, in the order it is worth reading. These become the const NARRATIVE = [
"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 = [
['attribution_reason', 'Attribution'], ['attribution_reason', 'Attribution'],
['eligibility_reason', 'Eligibility'], ['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'], ['kyc_mismatch_notes', 'KYC'],
['referral_analysis', 'Referral analysis'], ['referral_analysis', 'Referral analysis'],
['uw_decision_notes', 'Underwriting'], ['uw_decision_notes', 'Underwriting decision'],
['contact_notes', 'Call'],
['documents_notes', 'Documents'], ['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'], ['lost_reason', 'Why it was dropped'],
['resume_note', 'Why now'], ['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']) 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, stage: moved ? stage : null,
when: when(r.created_at), when: when(r.created_at),
docs, docs,
// The reasoning rungs of the chain of thought — the employee's own text. narrative: NARRATIVE
reasoning: REASONING
.map(([k, label]) => [label, byBase.get(k)?.value]) .map(([k, label]) => [label, byBase.get(k)?.value])
.filter(([, v]) => v !== undefined && v !== null && typeof v !== 'object' && String(v).trim() !== ''), .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] figures: [...byBase]
.filter(([base, f]) => MONEY.has(base) && f.value) .filter(([base, f]) => MONEY.has(base) && f.value)
.map(([base, f]) => [base.replace(/_/g, ' '), '₹' + Number(f.value).toLocaleString('en-IN')]), .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 last.when = it.when
// Keep the newest line as the preview — an operator scanning the trail // Keep the newest line as the preview — an operator scanning the trail
// wants where the conversation GOT to, not where it started. // 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) last.docs = last.docs.concat(it.docs)
continue continue
} }
@ -432,33 +407,7 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat, onOpenCall })
</div> </div>
) : null} ) : null}
{/* THE CHAIN OF THOUGHT. For an AI step, the reasoning is not a {it.narrative.map(([label, v]) => (
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]) => (
<div className="tl__say" key={label}> <div className="tl__say" key={label}>
<span className="tl__saylabel">{label}</span> <span className="tl__saylabel">{label}</span>
<ClampText text={String(v)} title={`${label} — ${it.what}`} lines={2} threshold={120} /> <ClampText text={String(v)} title={`${label} — ${it.what}`} lines={2} threshold={120} />