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} />