@sanity/ailf-studio 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -521,6 +521,15 @@ interface JudgmentData {
521
521
  /** Docs the task expected the model to use */
522
522
  canonicalDocs?: DocumentRef[];
523
523
  dimension: string;
524
+ /**
525
+ * `graderJudgments` manifest entry key = `formatEntryKey({mode, task,
526
+ * model, grader})` from the slim-report publisher. Present on reports
527
+ * published under W0051+. Consumed by `useArtifactDetail` in
528
+ * `JudgmentDetailDrawer` to hydrate the full reasoning from GCS, and by
529
+ * `JudgmentList` to drive hover-prefetch and the list row's data hook.
530
+ * Optional so legacy reports (pre-W0051) still type-check.
531
+ */
532
+ id?: string;
524
533
  modelId: string;
525
534
  /** True when the model failed to produce output (empty response, API error, refusal) */
526
535
  outputFailure?: boolean;
package/dist/index.js CHANGED
@@ -3280,7 +3280,7 @@ import {
3280
3280
  TabPanel as TabPanel3,
3281
3281
  Text as Text46
3282
3282
  } from "@sanity/ui";
3283
- import { useCallback as useCallback36 } from "react";
3283
+ import { useCallback as useCallback36, useEffect as useEffect15 } from "react";
3284
3284
  import { useRouter as useRouter3 } from "sanity/router";
3285
3285
 
3286
3286
  // src/lib/help-context.ts
@@ -7157,6 +7157,7 @@ function cacheKey({ runId, type, key }) {
7157
7157
  }
7158
7158
  var hydratedEntries = /* @__PURE__ */ new Map();
7159
7159
  var inFlight = /* @__PURE__ */ new Map();
7160
+ var knownNotFound = /* @__PURE__ */ new Set();
7160
7161
  var subscribers = /* @__PURE__ */ new Map();
7161
7162
  function getCached(k) {
7162
7163
  const hit = hydratedEntries.get(cacheKey(k));
@@ -7190,6 +7191,12 @@ function recordInFlight(k, start) {
7190
7191
  inFlight.set(composite, p);
7191
7192
  return p;
7192
7193
  }
7194
+ function markNotFound(k) {
7195
+ knownNotFound.add(cacheKey(k));
7196
+ }
7197
+ function isKnownNotFound(k) {
7198
+ return knownNotFound.has(cacheKey(k));
7199
+ }
7193
7200
  function notify(composite) {
7194
7201
  const set2 = subscribers.get(composite);
7195
7202
  if (!set2) return;
@@ -7197,6 +7204,14 @@ function notify(composite) {
7197
7204
  }
7198
7205
 
7199
7206
  // src/lib/artifact-fetch.ts
7207
+ var ArtifactHttpError = class extends Error {
7208
+ status;
7209
+ constructor(message, status) {
7210
+ super(message);
7211
+ this.name = "ArtifactHttpError";
7212
+ this.status = status;
7213
+ }
7214
+ };
7200
7215
  function buildSigningUrl(runId, type, entryKey) {
7201
7216
  const base = `${ARTIFACT_API_BASE_URL}/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}`;
7202
7217
  return entryKey === void 0 ? base : `${base}/${encodeURIComponent(entryKey)}`;
@@ -7205,8 +7220,9 @@ async function signAndFetchJson(signingUrl) {
7205
7220
  const signed = await exchangeSigningUrl(signingUrl);
7206
7221
  const res = await fetch(signed, { credentials: "omit" });
7207
7222
  if (!res.ok) {
7208
- throw new Error(
7209
- `GCS artifact fetch failed: ${res.status} ${res.statusText}`
7223
+ throw new ArtifactHttpError(
7224
+ `GCS artifact fetch failed: ${res.status} ${res.statusText}`,
7225
+ res.status
7210
7226
  );
7211
7227
  }
7212
7228
  return await res.json();
@@ -7215,8 +7231,9 @@ async function signAndFetchNdjson(signingUrl) {
7215
7231
  const signed = await exchangeSigningUrl(signingUrl);
7216
7232
  const res = await fetch(signed, { credentials: "omit" });
7217
7233
  if (!res.ok) {
7218
- throw new Error(
7219
- `GCS artifact fetch failed: ${res.status} ${res.statusText}`
7234
+ throw new ArtifactHttpError(
7235
+ `GCS artifact fetch failed: ${res.status} ${res.statusText}`,
7236
+ res.status
7220
7237
  );
7221
7238
  }
7222
7239
  const body = await res.text();
@@ -7271,8 +7288,9 @@ async function exchangeSigningUrl(signingUrl) {
7271
7288
  });
7272
7289
  if (!res.ok) {
7273
7290
  const body = await res.text().catch(() => "");
7274
- throw new Error(
7275
- `Artifact signing failed: ${res.status} ${res.statusText}${body ? ` \u2014 ${body.slice(0, 200)}` : ""}`
7291
+ throw new ArtifactHttpError(
7292
+ `Artifact signing failed: ${res.status} ${res.statusText}${body ? ` \u2014 ${body.slice(0, 200)}` : ""}`,
7293
+ res.status
7276
7294
  );
7277
7295
  }
7278
7296
  const envelope = await res.json();
@@ -7304,21 +7322,29 @@ function useArtifactDetail(type, key) {
7304
7322
  }, [runId, type, key]);
7305
7323
  const request = useCallback16(async () => {
7306
7324
  if (!runId || !ref || !key) return;
7307
- const hit = getCached({ runId, type, key });
7325
+ const cacheId = { runId, type, key };
7326
+ if (isKnownNotFound(cacheId)) {
7327
+ setStatus("error");
7328
+ return;
7329
+ }
7330
+ const hit = getCached(cacheId);
7308
7331
  if (hit !== null) {
7309
7332
  setStatus("ready");
7310
7333
  return;
7311
7334
  }
7312
7335
  setStatus("loading");
7313
7336
  try {
7314
- await recordInFlight({ runId, type, key }, async () => {
7337
+ await recordInFlight(cacheId, async () => {
7315
7338
  const url = buildSigningUrl(runId, type, key);
7316
7339
  const body = NDJSON_TYPES.has(type) ? await signAndFetchNdjson(url) : await signAndFetchJson(url);
7317
- setCached({ runId, type, key }, body);
7340
+ setCached(cacheId, body);
7318
7341
  return body;
7319
7342
  });
7320
7343
  setStatus("ready");
7321
- } catch {
7344
+ } catch (err) {
7345
+ if (err instanceof ArtifactHttpError && err.status === 404) {
7346
+ markNotFound(cacheId);
7347
+ }
7322
7348
  setStatus("error");
7323
7349
  }
7324
7350
  }, [runId, ref, type, key]);
@@ -7353,33 +7379,73 @@ import { useCallback as useCallback17, useMemo as useMemo7 } from "react";
7353
7379
  var NDJSON_TYPES2 = /* @__PURE__ */ new Set([
7354
7380
  "traces"
7355
7381
  ]);
7382
+ var HOVER_DEBOUNCE_MS = 200;
7383
+ var MAX_CONCURRENT_PREFETCHES = 4;
7384
+ var pendingHoverTimers = /* @__PURE__ */ new Map();
7385
+ var activePrefetches = 0;
7386
+ var prefetchWaitQueue = [];
7387
+ async function withPrefetchSlot(run) {
7388
+ if (activePrefetches >= MAX_CONCURRENT_PREFETCHES) {
7389
+ await new Promise((resolve) => prefetchWaitQueue.push(resolve));
7390
+ }
7391
+ activePrefetches++;
7392
+ try {
7393
+ return await run();
7394
+ } finally {
7395
+ activePrefetches--;
7396
+ const next = prefetchWaitQueue.shift();
7397
+ if (next) next();
7398
+ }
7399
+ }
7400
+ async function prefetchArtifactCore(args) {
7401
+ const { runId, type, key } = args;
7402
+ if (!runId || !key) return;
7403
+ const cacheId = { runId, type, key };
7404
+ if (isKnownNotFound(cacheId)) return;
7405
+ if (getCached(cacheId) !== null) return;
7406
+ try {
7407
+ await recordInFlight(
7408
+ cacheId,
7409
+ () => withPrefetchSlot(async () => {
7410
+ const url = buildSigningUrl(runId, type, key);
7411
+ const body = NDJSON_TYPES2.has(type) ? await signAndFetchNdjson(url) : await signAndFetchJson(url);
7412
+ setCached(cacheId, body);
7413
+ return body;
7414
+ })
7415
+ );
7416
+ } catch (err) {
7417
+ if (err instanceof ArtifactHttpError && err.status === 404) {
7418
+ markNotFound(cacheId);
7419
+ }
7420
+ }
7421
+ }
7422
+ function scheduleHoverPrefetch(type, run, debounceMs = HOVER_DEBOUNCE_MS) {
7423
+ const existing = pendingHoverTimers.get(type);
7424
+ if (existing) clearTimeout(existing);
7425
+ const timer = setTimeout(() => {
7426
+ pendingHoverTimers.delete(type);
7427
+ run();
7428
+ }, debounceMs);
7429
+ pendingHoverTimers.set(type, timer);
7430
+ }
7356
7431
  function useArtifactPrefetch(type) {
7357
7432
  const { runId } = useReportArtifactContext();
7358
7433
  const ref = useArtifactRef(type);
7359
7434
  const prefetch = useCallback17(
7360
7435
  async (key) => {
7361
7436
  if (!runId || !ref || !key) return;
7362
- const cached = getCached({ runId, type, key });
7363
- if (cached !== null) return;
7364
- try {
7365
- await recordInFlight({ runId, type, key }, async () => {
7366
- const url = buildSigningUrl(runId, type, key);
7367
- const body = NDJSON_TYPES2.has(type) ? await signAndFetchNdjson(url) : await signAndFetchJson(url);
7368
- setCached({ runId, type, key }, body);
7369
- return body;
7370
- });
7371
- } catch {
7372
- }
7437
+ await prefetchArtifactCore({ runId, type, key });
7373
7438
  },
7374
7439
  [runId, ref, type]
7375
7440
  );
7376
7441
  const onHover = useCallback17(
7377
7442
  (key) => {
7378
7443
  return () => {
7379
- void prefetch(key);
7444
+ if (!runId || !ref || !key) return;
7445
+ scheduleHoverPrefetch(type, () => void prefetch(key));
7380
7446
  };
7381
7447
  },
7382
- [prefetch]
7448
+ [runId, ref, type, prefetch]
7383
7449
  );
7384
7450
  const prefetchWindow = useCallback17(
7385
7451
  async (keys, centerIndex, radius = 5) => {
@@ -8194,12 +8260,15 @@ function JudgmentList({
8194
8260
  }
8195
8261
  }, [flatSlugs, activeRowSlug]);
8196
8262
  const { close: closeDrawer, open: openDrawer } = useJudgmentDrawer();
8263
+ const { runId: artifactRunId, manifest: artifactManifest } = useReportArtifactContext();
8197
8264
  useEffect9(() => {
8198
8265
  if (focus && focusedJudgment) {
8199
8266
  openDrawer({
8200
8267
  artifactCache,
8201
8268
  judgment: focusedJudgment,
8202
- testResult: focusedTestResult
8269
+ testResult: focusedTestResult,
8270
+ runId: artifactRunId,
8271
+ manifest: artifactManifest
8203
8272
  });
8204
8273
  } else if (!focus) {
8205
8274
  closeDrawer();
@@ -8210,7 +8279,9 @@ function JudgmentList({
8210
8279
  focusedTestResult,
8211
8280
  artifactCache,
8212
8281
  openDrawer,
8213
- closeDrawer
8282
+ closeDrawer,
8283
+ artifactRunId,
8284
+ artifactManifest
8214
8285
  ]);
8215
8286
  const prevFocusRef = useRef6(focus);
8216
8287
  useEffect9(() => {
@@ -8512,14 +8583,18 @@ function JudgmentCard({
8512
8583
  },
8513
8584
  [handleClick]
8514
8585
  );
8586
+ const onHoverTestOutputs = useMemo9(
8587
+ () => testOutputsKey ? testOutputsPrefetch.onHover(testOutputsKey) : null,
8588
+ [testOutputsPrefetch, testOutputsKey]
8589
+ );
8515
8590
  const handleMouseEnter = useCallback21(() => {
8516
8591
  row.handlers.onMouseEnter();
8517
- if (testOutputsKey) void testOutputsPrefetch.prefetch(testOutputsKey);
8518
- }, [row.handlers, testOutputsPrefetch, testOutputsKey]);
8592
+ onHoverTestOutputs?.();
8593
+ }, [row.handlers, onHoverTestOutputs]);
8519
8594
  const handleFocusPrefetch = useCallback21(() => {
8520
8595
  row.handlers.onFocus();
8521
- if (testOutputsKey) void testOutputsPrefetch.prefetch(testOutputsKey);
8522
- }, [row.handlers, testOutputsPrefetch, testOutputsKey]);
8596
+ onHoverTestOutputs?.();
8597
+ }, [row.handlers, onHoverTestOutputs]);
8523
8598
  return /* @__PURE__ */ jsx32(
8524
8599
  Box18,
8525
8600
  {
@@ -11222,6 +11297,16 @@ function JudgmentDetailDrawer({
11222
11297
  const rawTaskName = sep > 0 ? judgment.taskId.substring(sep + 3) : judgment.taskId;
11223
11298
  const { name: taskName, variant } = splitVariant(rawTaskName);
11224
11299
  const dimLabel = dimensionLabel2(judgment.dimension);
11300
+ const judgmentId = judgment.id ?? "";
11301
+ const [fullJudgment, fullStatus, requestFullJudgment] = useArtifactDetail("graderJudgments", judgmentId);
11302
+ useEffect12(() => {
11303
+ if (!judgmentId) return;
11304
+ if (fullStatus !== "idle") return;
11305
+ void requestFullJudgment();
11306
+ }, [judgmentId, fullStatus, requestFullJudgment]);
11307
+ const fullReason = typeof fullJudgment?.reason === "string" && fullJudgment.reason.length > 0 ? fullJudgment.reason : null;
11308
+ const reasoningText = fullReason ?? judgment.reason;
11309
+ const reasoningIsPreview = fullReason === null && judgmentId.length > 0 && fullStatus !== "error";
11225
11310
  useEffect12(() => {
11226
11311
  setTab("reasoning");
11227
11312
  }, [judgment.taskId, judgment.dimension, judgment.modelId]);
@@ -11453,10 +11538,11 @@ function JudgmentDetailDrawer({
11453
11538
  {
11454
11539
  copiedLabel: "Reasoning copied",
11455
11540
  label: "Copy reasoning",
11456
- text: judgment.reason
11541
+ text: reasoningText
11457
11542
  }
11458
11543
  ) }),
11459
- /* @__PURE__ */ jsx59(Markdown, { content: judgment.reason })
11544
+ /* @__PURE__ */ jsx59(Markdown, { content: reasoningText }),
11545
+ reasoningIsPreview && /* @__PURE__ */ jsx59(Text44, { muted: true, size: 1, style: { marginTop: 8 }, children: fullStatus === "loading" ? "Loading full reasoning\u2026" : "Showing preview (280 chars). Full reasoning not yet loaded." })
11460
11546
  ] })
11461
11547
  }
11462
11548
  ),
@@ -11629,7 +11715,7 @@ function JudgmentDetailDrawerOutlet({
11629
11715
  flexDirection: "column",
11630
11716
  overflow: "hidden"
11631
11717
  },
11632
- children: /* @__PURE__ */ jsx60(
11718
+ children: /* @__PURE__ */ jsx60(ReportArtifactProvider, { runId: active.runId, manifest: active.manifest, children: /* @__PURE__ */ jsx60(
11633
11719
  JudgmentDetailDrawer,
11634
11720
  {
11635
11721
  artifactCache: active.artifactCache,
@@ -11637,7 +11723,7 @@ function JudgmentDetailDrawerOutlet({
11637
11723
  onClose,
11638
11724
  testResult: active.testResult
11639
11725
  }
11640
- )
11726
+ ) })
11641
11727
  }
11642
11728
  )
11643
11729
  ]
@@ -11873,17 +11959,29 @@ var VIEW_PARAM_MAP = {
11873
11959
  timeline: "timeline"
11874
11960
  };
11875
11961
  function Dashboard() {
11962
+ return /* @__PURE__ */ jsx62(HelpProvider, { children: /* @__PURE__ */ jsx62(JudgmentDrawerProvider, { children: /* @__PURE__ */ jsx62(DashboardShell, {}) }) });
11963
+ }
11964
+ function DashboardShell() {
11876
11965
  const router = useRouter3();
11966
+ const { close: closeDrawer } = useJudgmentDrawer();
11967
+ const routerState = router.state;
11968
+ const reportId = routerState.reportId ?? null;
11969
+ useEffect15(() => {
11970
+ if (!reportId) closeDrawer();
11971
+ }, [reportId, closeDrawer]);
11877
11972
  const handleJudgmentDrawerClose = useCallback36(() => {
11973
+ closeDrawer();
11878
11974
  const state = { ...router.state };
11879
- delete state.focus;
11880
- router.navigate(state);
11881
- }, [router]);
11882
- return /* @__PURE__ */ jsx62(HelpProvider, { children: /* @__PURE__ */ jsx62(JudgmentDrawerProvider, { children: /* @__PURE__ */ jsxs45(Flex34, { style: { height: "100%" }, children: [
11975
+ if (state.focus) {
11976
+ delete state.focus;
11977
+ router.navigate(state);
11978
+ }
11979
+ }, [closeDrawer, router]);
11980
+ return /* @__PURE__ */ jsxs45(Flex34, { style: { height: "100%" }, children: [
11883
11981
  /* @__PURE__ */ jsx62(Box29, { flex: 1, overflow: "auto", children: /* @__PURE__ */ jsx62(DashboardContent, {}) }),
11884
11982
  /* @__PURE__ */ jsx62(JudgmentDetailDrawerOutlet, { onClose: handleJudgmentDrawerClose }),
11885
11983
  /* @__PURE__ */ jsx62(HelpDrawer, {})
11886
- ] }) }) });
11984
+ ] });
11887
11985
  }
11888
11986
  function DashboardContent() {
11889
11987
  const router = useRouter3();
@@ -12016,7 +12114,7 @@ function ailfTool(options = {}) {
12016
12114
  // src/actions/RunEvaluationAction.tsx
12017
12115
  import { BarChartIcon as BarChartIcon2 } from "@sanity/icons";
12018
12116
  import { useToast as useToast10 } from "@sanity/ui";
12019
- import { useCallback as useCallback37, useEffect as useEffect15, useRef as useRef9, useState as useState27 } from "react";
12117
+ import { useCallback as useCallback37, useEffect as useEffect16, useRef as useRef9, useState as useState27 } from "react";
12020
12118
  import {
12021
12119
  getReleaseIdFromReleaseDocumentId as getReleaseIdFromReleaseDocumentId3,
12022
12120
  useClient as useClient12,
@@ -12050,7 +12148,7 @@ function createRunEvaluationAction(options = {}) {
12050
12148
  const [state, setState] = useState27({ status: "loading" });
12051
12149
  const requestedAtRef = useRef9(null);
12052
12150
  const perspectiveId = getReleaseIdFromReleaseDocumentId3(release._id);
12053
- useEffect15(() => {
12151
+ useEffect16(() => {
12054
12152
  let cancelled = false;
12055
12153
  client.fetch(contentImpactQuery, buildReportQueryParams(perspectiveId)).then((results) => {
12056
12154
  if (cancelled) return;
@@ -12073,7 +12171,7 @@ function createRunEvaluationAction(options = {}) {
12073
12171
  cancelled = true;
12074
12172
  };
12075
12173
  }, [client, perspectiveId]);
12076
- useEffect15(() => {
12174
+ useEffect16(() => {
12077
12175
  if (state.status !== "requested" && state.status !== "polling") return;
12078
12176
  const { requestId, startedAt } = state;
12079
12177
  if (state.status === "requested") {
@@ -12123,7 +12221,7 @@ function createRunEvaluationAction(options = {}) {
12123
12221
  }, POLL_INTERVAL_MS2);
12124
12222
  return () => clearInterval(interval);
12125
12223
  }, [client, perspectiveId, state]);
12126
- useEffect15(() => {
12224
+ useEffect16(() => {
12127
12225
  if (state.status !== "error") return;
12128
12226
  const timer = setTimeout(() => {
12129
12227
  client.fetch(contentImpactQuery, buildReportQueryParams(perspectiveId)).then((results) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ailf-studio",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "AI Literacy Framework — Sanity Studio dashboard plugin",
5
5
  "type": "module",
6
6
  "license": "MIT",