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:
parent
134db9a06c
commit
92c6988b80
@ -6,7 +6,12 @@ import { KIND_LABEL, RV, STATE_TONE } from '../api/config';
|
||||
import type { Tone } from '../api/config';
|
||||
import type { AiDecision, AuditEntry, Row, SimRow } from '../api/types';
|
||||
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 }) {
|
||||
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>
|
||||
{inc !== 0 && <div className={`tnum shrink-0 text-lg font-semibold ${inc > 0 ? 'text-accent' : 'text-bad'}`}>{inr(inc, { sign: true })}</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 && (
|
||||
<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}
|
||||
@ -175,7 +180,9 @@ export function SimTable({ rows, hasHypo, loading, error }: { rows: SimRow[] | n
|
||||
export function InstanceTrace({ instanceId }: { instanceId: number | string }) {
|
||||
const { client } = useZino();
|
||||
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 (
|
||||
<Card title="Trail" subtitle="Every step on this record, and what the AI reasoned.">
|
||||
{audit.loading && !audit.data && <Spinner />}
|
||||
@ -193,8 +200,8 @@ export function InstanceTrace({ instanceId }: { instanceId: number | string }) {
|
||||
</div>
|
||||
{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">
|
||||
{a.fields.filter((f) => f.value !== '' && f.value != null).slice(0, 12).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>
|
||||
{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 ${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>
|
||||
)}
|
||||
|
||||
@ -43,3 +43,18 @@ export function parseJson<T>(v: unknown, fallback: T): T {
|
||||
if (typeof v === 'object') return v as T;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { ACT, RV, WF } from '../../api/config';
|
||||
import type { Row } from '../../api/types';
|
||||
import { ActivityForm } from '../../components/ActivityForm';
|
||||
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';
|
||||
|
||||
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="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 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><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">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">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)` : ''}{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">Act by</div><div className="text-ink">{date(o.advice_deadline)}</div></div>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,7 @@ import { useQuery, useZino } from '../../api/provider';
|
||||
import { ACT, RV, WF } from '../../api/config';
|
||||
import { ActivityForm } from '../../components/ActivityForm';
|
||||
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 { useScope } from './scope';
|
||||
|
||||
@ -68,7 +68,7 @@ export function AdvisoryDetail() {
|
||||
<div className="space-y-5 xl:col-span-2">
|
||||
{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>
|
||||
</>
|
||||
) : (
|
||||
@ -78,7 +78,7 @@ export function AdvisoryDetail() {
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user