Render the AI's bold prose, hide engine bookkeeping in the trail, drop the Studio-only monitor call

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-10-22 16:48:29 +05:30
parent 134db9a06c
commit 92c6988b80
4 changed files with 34 additions and 12 deletions

View File

@ -6,7 +6,12 @@ import { KIND_LABEL, RV, STATE_TONE } from '../api/config';
import type { Tone } from '../api/config'; import type { Tone } from '../api/config';
import type { AiDecision, AuditEntry, Row, SimRow } from '../api/types'; import type { AiDecision, AuditEntry, Row, SimRow } from '../api/types';
import { Badge, Bar, Card, Empty, ErrorNote, Spinner } from './core'; import { Badge, Bar, Card, Empty, ErrorNote, Spinner } from './core';
import { date, dateTime, inr, mt, num, pct } from '../format'; import { date, dateTime, inr, mdParts, mt, num, pct } from '../format';
/** AI prose with its **bold** honoured. */
export function Prose({ text, className = '' }: { text: unknown; className?: string }) {
return <p className={`whitespace-pre-line ${className}`}>{mdParts(text).map((x, i) => (x.bold ? <b key={i} className="font-semibold text-ink">{x.text}</b> : <span key={i}>{x.text}</span>))}</p>;
}
export function StateBadge({ state }: { state: unknown }) { export function StateBadge({ state }: { state: unknown }) {
const s = String(state ?? ''); const s = String(state ?? '');
@ -75,7 +80,7 @@ export function RecCard({ rec, compact = false }: { rec: Row; compact?: boolean
<p className="text-sm font-semibold text-ink">{String(rec.headline ?? '')}</p> <p className="text-sm font-semibold text-ink">{String(rec.headline ?? '')}</p>
{inc !== 0 && <div className={`tnum shrink-0 text-lg font-semibold ${inc > 0 ? 'text-accent' : 'text-bad'}`}>{inr(inc, { sign: true })}</div>} {inc !== 0 && <div className={`tnum shrink-0 text-lg font-semibold ${inc > 0 ? 'text-accent' : 'text-bad'}`}>{inr(inc, { sign: true })}</div>}
</div> </div>
{!compact && rec.rationale_md ? <p className="mt-2 text-sm whitespace-pre-line text-muted">{String(rec.rationale_md)}</p> : null} {!compact && rec.rationale_md ? <Prose text={rec.rationale_md} className="mt-2 text-sm text-muted" /> : null}
{!compact && ( {!compact && (
<div className="mt-3 flex flex-wrap gap-x-5 gap-y-1 text-xs text-faint"> <div className="mt-3 flex flex-wrap gap-x-5 gap-y-1 text-xs text-faint">
{rec.delta_mt ? <span>Add <b className="text-ink">{mt(rec.delta_mt)}</b></span> : null} {rec.delta_mt ? <span>Add <b className="text-ink">{mt(rec.delta_mt)}</b></span> : null}
@ -175,7 +180,9 @@ export function SimTable({ rows, hasHypo, loading, error }: { rows: SimRow[] | n
export function InstanceTrace({ instanceId }: { instanceId: number | string }) { export function InstanceTrace({ instanceId }: { instanceId: number | string }) {
const { client } = useZino(); const { client } = useZino();
const audit = useQuery(() => client.audit(instanceId), [instanceId], true, 15000); const audit = useQuery(() => client.audit(instanceId), [instanceId], true, 15000);
const ai = useQuery(() => client.aiDecisions(instanceId).catch(() => [] as AiDecision[]), [instanceId], true, 15000); // The AI's rationale is on the audit row (ai_reasoning) and in the fields it submitted;
// /monitor/* is Studio-scoped on this build (403 for app users), so it is not called.
const ai = { data: [] as AiDecision[] };
return ( return (
<Card title="Trail" subtitle="Every step on this record, and what the AI reasoned."> <Card title="Trail" subtitle="Every step on this record, and what the AI reasoned.">
{audit.loading && !audit.data && <Spinner />} {audit.loading && !audit.data && <Spinner />}
@ -193,8 +200,8 @@ export function InstanceTrace({ instanceId }: { instanceId: number | string }) {
</div> </div>
{a.fields && a.fields.length > 0 && ( {a.fields && a.fields.length > 0 && (
<dl className="mt-1.5 grid grid-cols-1 gap-x-4 gap-y-1 text-xs sm:grid-cols-2"> <dl className="mt-1.5 grid grid-cols-1 gap-x-4 gap-y-1 text-xs sm:grid-cols-2">
{a.fields.filter((f) => f.value !== '' && f.value != null).slice(0, 12).map((f) => ( {a.fields.filter((f) => f.value !== '' && f.value != null && !f.field_id.startsWith('_') && !f.label.startsWith('_')).slice(0, 14).map((f) => (
<div key={f.field_id} className="min-w-0"><dt className="inline text-faint">{f.label}: </dt><dd className="inline break-words text-ink">{String(f.value).slice(0, 300)}</dd></div> <div key={f.field_id} className={`min-w-0 ${String(f.value).length > 160 ? 'sm:col-span-2' : ''}`}><dt className="inline text-faint">{f.label}: </dt><dd className="inline break-words whitespace-pre-line text-ink">{String(f.value).length > 700 ? String(f.value).slice(0, 700) + '…' : String(f.value)}</dd></div>
))} ))}
</dl> </dl>
)} )}

View File

@ -43,3 +43,18 @@ export function parseJson<T>(v: unknown, fallback: T): T {
if (typeof v === 'object') return v as T; if (typeof v === 'object') return v as T;
try { return JSON.parse(String(v)) as T; } catch { return fallback; } try { return JSON.parse(String(v)) as T; } catch { return fallback; }
} }
/** The AI writes **bold** in its prose; render just that, as React nodes. */
export function mdParts(text: unknown): Array<{ bold: boolean; text: string }> {
const t = String(text ?? '');
const out: Array<{ bold: boolean; text: string }> = [];
const re = /\*\*([^*]+)\*\*/g;
let last = 0; let m: RegExpExecArray | null;
while ((m = re.exec(t))) {
if (m.index > last) out.push({ bold: false, text: t.slice(last, m.index) });
out.push({ bold: true, text: m[1] });
last = m.index + m[0].length;
}
if (last < t.length) out.push({ bold: false, text: t.slice(last) });
return out;
}

View File

@ -6,7 +6,7 @@ import { ACT, RV, WF } from '../../api/config';
import type { Row } from '../../api/types'; import type { Row } from '../../api/types';
import { ActivityForm } from '../../components/ActivityForm'; import { ActivityForm } from '../../components/ActivityForm';
import { Button, Card, Empty, ErrorNote, Facts, Spinner } from '../../components/core'; import { Button, Card, Empty, ErrorNote, Facts, Spinner } from '../../components/core';
import { DataTable, InstanceTrace, KindBadge, RecCard, StateBadge } from '../../components/widgets'; import { DataTable, InstanceTrace, KindBadge, Prose, RecCard, StateBadge } from '../../components/widgets';
import { date, inr, mt, num, pct } from '../../format'; import { date, inr, mt, num, pct } from '../../format';
function useOrders(customerNo: string | null | undefined, refetchMs?: number) { function useOrders(customerNo: string | null | undefined, refetchMs?: number) {
@ -80,10 +80,10 @@ export function OrderDetail() {
<div className="rounded-lg border border-ai-edge bg-ai-soft p-4"> <div className="rounded-lg border border-ai-edge bg-ai-soft p-4">
<div className="flex flex-wrap items-center gap-2"><KindBadge kind={o.advice_kind} />{o.suggested_scheme ? <span className="text-xs text-faint">{String(o.suggested_scheme)}</span> : null}{inc !== 0 && <span className={`tnum ml-auto text-lg font-semibold ${inc > 0 ? 'text-accent' : 'text-bad'}`}>{inr(inc, { sign: true })}</span>}</div> <div className="flex flex-wrap items-center gap-2"><KindBadge kind={o.advice_kind} />{o.suggested_scheme ? <span className="text-xs text-faint">{String(o.suggested_scheme)}</span> : null}{inc !== 0 && <span className={`tnum ml-auto text-lg font-semibold ${inc > 0 ? 'text-accent' : 'text-bad'}`}>{inr(inc, { sign: true })}</span>}</div>
<p className="mt-2 text-sm font-semibold text-ink">{String(o.advice_headline ?? '')}</p> <p className="mt-2 text-sm font-semibold text-ink">{String(o.advice_headline ?? '')}</p>
<p className="mt-2 text-sm whitespace-pre-line text-ink">{String(o.advice_rationale ?? '')}</p> <Prose text={o.advice_rationale} className="mt-2 text-sm text-ink" />
<div className="mt-3 grid grid-cols-2 gap-x-6 gap-y-2 text-xs sm:grid-cols-4"> <div className="mt-3 grid grid-cols-2 gap-x-6 gap-y-2 text-xs sm:grid-cols-4">
<div><div className="text-faint">Keyed</div><div className="tnum text-ink">{mt(o.qty_mt)} · {inr(o.payout_now)}</div></div> <div><div className="text-faint">Keyed</div><div className="tnum text-ink">{mt(o.qty_mt)}{num(o.payout_now) > 0 ? ` · ${inr(o.payout_now)}` : ''}</div></div>
<div><div className="text-faint">Suggested</div><div className="tnum text-ink">{suggested != null ? mt(suggested) : '—'}{o.suggested_pp_mt ? ` (${mt(o.suggested_pp_mt)} premium)` : ''} · {inr(o.payout_if)}</div></div> <div><div className="text-faint">Suggested</div><div className="tnum text-ink">{suggested != null ? mt(suggested) : '—'}{o.suggested_pp_mt ? ` (${mt(o.suggested_pp_mt)} premium)` : ''}{num(o.payout_if) > 0 ? ` · ${inr(o.payout_if)}` : ''}</div></div>
<div><div className="text-faint">Ship to</div><div className="text-ink">{String(o.suggested_ship_to || o.ship_to_code || '—')}</div></div> <div><div className="text-faint">Ship to</div><div className="text-ink">{String(o.suggested_ship_to || o.ship_to_code || '—')}</div></div>
<div><div className="text-faint">Act by</div><div className="text-ink">{date(o.advice_deadline)}</div></div> <div><div className="text-faint">Act by</div><div className="text-ink">{date(o.advice_deadline)}</div></div>
</div> </div>

View File

@ -5,7 +5,7 @@ import { useQuery, useZino } from '../../api/provider';
import { ACT, RV, WF } from '../../api/config'; import { ACT, RV, WF } from '../../api/config';
import { ActivityForm } from '../../components/ActivityForm'; import { ActivityForm } from '../../components/ActivityForm';
import { Button, Card, Empty, ErrorNote, Facts, Kpi, Spinner } from '../../components/core'; import { Button, Card, Empty, ErrorNote, Facts, Kpi, Spinner } from '../../components/core';
import { DataTable, InstanceTrace, KindBadge, RecCard, StateBadge } from '../../components/widgets'; import { DataTable, InstanceTrace, KindBadge, Prose, RecCard, StateBadge } from '../../components/widgets';
import { date, inr, mt, num, pct } from '../../format'; import { date, inr, mt, num, pct } from '../../format';
import { useScope } from './scope'; import { useScope } from './scope';
@ -68,7 +68,7 @@ export function AdvisoryDetail() {
<div className="space-y-5 xl:col-span-2"> <div className="space-y-5 xl:col-span-2">
{isSweep ? ( {isSweep ? (
<> <>
<Card title="Commentary"><p className="p-4 text-sm whitespace-pre-line text-ink">{String(a.commentary ?? '') || 'Waiting…'}</p></Card> <Card title="Commentary"><div className="p-4 text-sm text-ink">{a.commentary ? <Prose text={a.commentary} /> : 'Waiting…'}</div></Card>
<Card title="Nudge list" subtitle="One line per dealer, ranked by what the nudge is worth. Send as is."><pre className="overflow-x-auto p-4 font-sans text-sm whitespace-pre-wrap text-ink">{String(a.nudge_list ?? '')}</pre></Card> <Card title="Nudge list" subtitle="One line per dealer, ranked by what the nudge is worth. Send as is."><pre className="overflow-x-auto p-4 font-sans text-sm whitespace-pre-wrap text-ink">{String(a.nudge_list ?? '')}</pre></Card>
</> </>
) : ( ) : (
@ -78,7 +78,7 @@ export function AdvisoryDetail() {
<div className="rounded-lg border border-ai-edge bg-ai-soft p-4"> <div className="rounded-lg border border-ai-edge bg-ai-soft p-4">
<div className="flex flex-wrap items-center gap-2"><KindBadge kind={a.advice_kind} />{a.suggested_scheme ? <span className="text-xs text-faint">{String(a.suggested_scheme)}</span> : null}{a.incremental_payout != null && <span className="tnum ml-auto text-lg font-semibold text-accent">{inr(a.incremental_payout, { sign: true })}</span>}</div> <div className="flex flex-wrap items-center gap-2"><KindBadge kind={a.advice_kind} />{a.suggested_scheme ? <span className="text-xs text-faint">{String(a.suggested_scheme)}</span> : null}{a.incremental_payout != null && <span className="tnum ml-auto text-lg font-semibold text-accent">{inr(a.incremental_payout, { sign: true })}</span>}</div>
<p className="mt-2 text-sm font-semibold text-ink">{String(a.advice_headline ?? '')}</p> <p className="mt-2 text-sm font-semibold text-ink">{String(a.advice_headline ?? '')}</p>
<p className="mt-2 text-sm whitespace-pre-line text-ink">{String(a.advice_rationale ?? '')}</p> <Prose text={a.advice_rationale} className="mt-2 text-sm text-ink" />
<div className="mt-3 flex flex-wrap gap-x-6 text-xs text-faint">{a.suggested_qty_mt ? <span>Add <b className="text-ink">{mt(a.suggested_qty_mt)}</b></span> : null}{a.suggested_pp_mt ? <span>Premium <b className="text-ink">{mt(a.suggested_pp_mt)}</b></span> : null}{a.advice_deadline ? <span>By <b className="text-ink">{date(a.advice_deadline)}</b></span> : null}{a.advice_confidence ? <span>Confidence {pct(num(a.advice_confidence) * 100, 0)}</span> : null}</div> <div className="mt-3 flex flex-wrap gap-x-6 text-xs text-faint">{a.suggested_qty_mt ? <span>Add <b className="text-ink">{mt(a.suggested_qty_mt)}</b></span> : null}{a.suggested_pp_mt ? <span>Premium <b className="text-ink">{mt(a.suggested_pp_mt)}</b></span> : null}{a.advice_deadline ? <span>By <b className="text-ink">{date(a.advice_deadline)}</b></span> : null}{a.advice_confidence ? <span>Confidence {pct(num(a.advice_confidence) * 100, 0)}</span> : null}</div>
</div> </div>
)} )}