diff --git a/src/components/ChainOfThought.css b/src/components/ChainOfThought.css index 1468237..7a30fe4 100644 --- a/src/components/ChainOfThought.css +++ b/src/components/ChainOfThought.css @@ -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; } + diff --git a/src/components/ChainOfThought.jsx b/src/components/ChainOfThought.jsx index 22fd699..e2c15b9 100644 --- a/src/components/ChainOfThought.jsx +++ b/src/components/ChainOfThought.jsx @@ -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 (
@@ -93,7 +50,7 @@ export default function ChainOfThought({ reasoning, tools, decided, confidence, fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" /> Chain of thought - · {steps.length} step{steps.length === 1 ? '' : 's'} + · {steps} step{steps === 1 ? '' : 's'}
    - {toolPills.length ? ( -
  1. - Checked -
    - {toolPills.map((t, i) => ( - - {t.name} - - ))} -
    -
  2. - ) : null} - {reasoning.map(([label, text], i) => (
  3. {i === 0 ? 'Reasoned' : label} @@ -127,10 +71,11 @@ export default function ChainOfThought({ reasoning, tools, decided, confidence,
    {decided.what} {decided.stage ? <> — moved to {decided.stage} : null} - {(confidence != null || model) ? ( + {confidence != null ? (
    - {confidence != null ? {Math.round(confidence * (confidence <= 1 ? 100 : 1))}% confidence : null} - {model ? {model} : null} + + {Math.round(confidence * (confidence <= 1 ? 100 : 1))}% confidence +
    ) : null}
    diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 9362457..89a30bd 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -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 ? ( ) : null}