From 0bf414cd601d417a0f763e530c64130a79390869 Mon Sep 17 00:00:00 2001 From: Hizrian Date: Thu, 28 May 2026 11:59:15 +0700 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20AIN-251=20+=20AIN-266=20W4?= =?UTF-8?q?=20=E2=80=94=20shared=20=20on=203=20list=20pages=20?= =?UTF-8?q?+=20candidates=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W4/9 SHIP-NOW (focused subset of 9 dashboard tickets; the larger feature PRs — AIN-258/259/260/256/257/261 — are tracked as W4-follow-up after the W3 backend they consume lands on prod, which it just did). AIN-251 — shared adoption: - agents/page.tsx — added (was missing) - inferences/page.tsx — replaced inline
with shared component for cross-page parity - workflows/page.tsx — added (was missing) The list-pages-now-have-crumbs gap is closed. The 5 detail/sub pages already used the shared component; the 3 list pages were drift. AIN-266 W4 — candidates card on /inferences/[id]: - types.ts: new InferenceCandidate type + candidates field on InferenceDetail (mirrors api W3 response shape; Decimals as strings to preserve precision) - InferenceDetailClient.tsx: candidates card between §16 Outcome record and the Audit chain block. Renders only when api returned a non-empty list (null on pre-§16 rows + pinned passthroughs). - Per-row: rank · model_slug (with `chosen` badge on the winner) · brand_slug · q_prior · projected_cost · eligible · drop_reason. - Tailwind classes match the existing detail-page palette (text-ink, bg-bg-elev, hairline borders) so brand v1.3 ramp is preserved. Validation: - pnpm --filter dashboard typecheck: passed (no errors) - §D3 lock preserved: api W3 PR #90 guarded /v1/audit/public; dashboard consumes via /v1/inferences/{id} which is already authed-tenant-only via getOwnerHandle → SSR proxy → API key bearer. Deferred to W4-follow-up (each is a substantive new feature surface, better as separate review): - AIN-258 /workflows trace filter-row + 2-col master-detail - AIN-259 /agents/[id] Identity card - AIN-260 /agents/new template-tile grid + 5-field caps + Key & deploy - AIN-256 /templates search-row + featured card + dashed placeholder - AIN-257 /templates editor consolidation + Tailwind→chrome.css migration - AIN-261 /inferences/[id] prev/next-step nav - AIN-262 brand v1.3 token reconciliation (touches MANY files; best as its own PR) Refs: AIN-251 · AIN-266 · §D3 · W3 backend live on PROD Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/dashboard/app/(tenant)/agents/page.tsx | 9 +++ .../inferences/[id]/InferenceDetailClient.tsx | 68 +++++++++++++++++++ .../app/(tenant)/inferences/[id]/types.ts | 21 ++++++ .../app/(tenant)/inferences/page.tsx | 18 ++--- .../dashboard/app/(tenant)/workflows/page.tsx | 9 +++ 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/apps/dashboard/app/(tenant)/agents/page.tsx b/apps/dashboard/app/(tenant)/agents/page.tsx index bbb2ad1..39e8010 100644 --- a/apps/dashboard/app/(tenant)/agents/page.tsx +++ b/apps/dashboard/app/(tenant)/agents/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { CopyPill } from "@ainfera/ui"; +import { Crumbs } from "@/components/v15/Crumbs"; import { getOwnerHandle } from "@/lib/ainfera/proxy"; import { listAgents } from "@/lib/v15/api"; @@ -13,6 +14,14 @@ export default async function AgentsPage() { return ( <> + {/* AIN-251 W4 · shared for cross-page parity. */} + +

Agents

diff --git a/apps/dashboard/app/(tenant)/inferences/[id]/InferenceDetailClient.tsx b/apps/dashboard/app/(tenant)/inferences/[id]/InferenceDetailClient.tsx index 4502527..beadccc 100644 --- a/apps/dashboard/app/(tenant)/inferences/[id]/InferenceDetailClient.tsx +++ b/apps/dashboard/app/(tenant)/inferences/[id]/InferenceDetailClient.tsx @@ -215,6 +215,74 @@ export function InferenceDetailClient({ inferenceId, initial }: Props) { ) : null}
+ {/* AIN-266 W3 · candidates card — the routing decision artifact. + * Surfaces every model Ainfera Inference considered for this call, + * the rank order, q_prior, projected cost, and drop reasons. + * Renders only when the api returned a non-empty candidates list + * (null on pre-§16 rows + pinned-passthroughs that bypass the + * brain). Authed-tenant-only on the wire (§D3 lock). */} + {inf.candidates && inf.candidates.length > 0 ? ( +
+
+

+ Candidates ({inf.candidates.length}) +

+ + the decision · ranked + +
+
+ + + + + + + + + + + + + + {inf.candidates.map((c, i) => { + const chosen = c.model_slug === inf.model_used; + return ( + + + + + + + + + + ); + })} + +
#ModelBrandq_priorproj. costeligibledrop reason
{c.rank ?? i + 1} + {c.model_slug ?? "—"} + {chosen ? ( + + chosen + + ) : null} + {c.brand_slug ?? "—"}{c.q_prior ?? "—"} + {c.projected_cost_usd ? fmtCost(c.projected_cost_usd) : "—"} + + {c.m_allowed === true ? "yes" : c.m_allowed === false ? "no" : "—"} + {c.drop_reason ?? "—"}
+
+
+ ) : null} + {/* Audit chain block — AIN-182 §4 "Verify with curl" requirement. */}
diff --git a/apps/dashboard/app/(tenant)/inferences/[id]/types.ts b/apps/dashboard/app/(tenant)/inferences/[id]/types.ts index 327b044..1cb48f9 100644 --- a/apps/dashboard/app/(tenant)/inferences/[id]/types.ts +++ b/apps/dashboard/app/(tenant)/inferences/[id]/types.ts @@ -35,4 +35,25 @@ export type InferenceDetail = { policy_version?: string | null; cell?: string | null; routing_rationale?: string | null; + /** AIN-266 W3 · routing candidate set. Mirrors §16 + * routing_outcomes.candidates JSONB. NULL on pre-2026-05-21 rows + + * pinned-passthrough calls. AUTHED-tenant-only on the wire (§D3 lock + * — never on /v1/audit/public). */ + candidates?: InferenceCandidate[] | null; +}; + +/** One row of the §16 candidate set. Keys mirror the JSONB shape on + * routing_outcomes exactly. Decimals come over as strings to preserve + * precision through JSON. */ +export type InferenceCandidate = { + model_id?: string | null; + model_slug?: string | null; + brand_slug?: string | null; + q_prior?: string | null; + price_in_per_mtok_usd?: string | null; + price_out_per_mtok_usd?: string | null; + m_allowed?: boolean | null; + projected_cost_usd?: string | null; + drop_reason?: string | null; + rank?: number | null; }; diff --git a/apps/dashboard/app/(tenant)/inferences/page.tsx b/apps/dashboard/app/(tenant)/inferences/page.tsx index 16785a8..5bd71c0 100644 --- a/apps/dashboard/app/(tenant)/inferences/page.tsx +++ b/apps/dashboard/app/(tenant)/inferences/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import Link from "next/link"; +import { Crumbs } from "@/components/v15/Crumbs"; import { getOwnerHandle } from "@/lib/ainfera/proxy"; import { publicAuditFeed, publicLeaderboard } from "@/lib/v15/api"; @@ -25,15 +26,14 @@ export default async function InferencesIndexPage() { return ( <> -
-
-
- workspace - / - inferences -
-
-
+ {/* AIN-251 W4 · shared for cross-page parity. */} + +
diff --git a/apps/dashboard/app/(tenant)/workflows/page.tsx b/apps/dashboard/app/(tenant)/workflows/page.tsx index de8d940..ced96a9 100644 --- a/apps/dashboard/app/(tenant)/workflows/page.tsx +++ b/apps/dashboard/app/(tenant)/workflows/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import Link from "next/link"; +import { Crumbs } from "@/components/v15/Crumbs"; import { getOwnerHandle } from "@/lib/ainfera/proxy"; import { listWorkflows } from "@/lib/v15/api"; @@ -12,6 +13,14 @@ export default async function WorkflowsPage() { return ( <> + {/* AIN-251 W4 · shared for cross-page parity. */} + +

Workflows