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 { 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} />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user