@sanity/ailf-studio 1.12.0 → 1.13.0

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +14 -0
  2. package/dist/index.js +1155 -222
  3. package/package.json +8 -2
package/dist/index.js CHANGED
@@ -3534,18 +3534,18 @@ import { route } from "sanity/router";
3534
3534
  // src/components/Dashboard.tsx
3535
3535
  import { HelpCircleIcon as HelpCircleIcon8 } from "@sanity/icons";
3536
3536
  import {
3537
- Box as Box34,
3538
- Button as Button13,
3537
+ Box as Box37,
3538
+ Button as Button15,
3539
3539
  Container,
3540
- Flex as Flex37,
3541
- Stack as Stack40,
3540
+ Flex as Flex39,
3541
+ Stack as Stack42,
3542
3542
  Tab as Tab3,
3543
3543
  TabList as TabList3,
3544
3544
  TabPanel as TabPanel3,
3545
- Text as Text51
3545
+ Text as Text53
3546
3546
  } from "@sanity/ui";
3547
- import { useCallback as useCallback39, useEffect as useEffect17 } from "react";
3548
- import { useRouter as useRouter4 } from "sanity/router";
3547
+ import { useCallback as useCallback42, useEffect as useEffect18 } from "react";
3548
+ import { useRouter as useRouter5 } from "sanity/router";
3549
3549
 
3550
3550
  // src/lib/help-context.ts
3551
3551
  import {
@@ -4225,9 +4225,12 @@ var scoreTimelineQuery = (
4225
4225
  && completedAt >= $startDate
4226
4226
  ${filterSourceClause("$source")}
4227
4227
  ${filterModeClause("$mode")}
4228
+ ${filterOwnerTeamClause("$ownerTeam")}
4228
4229
  ] | order(completedAt asc) {
4229
4230
  _id,
4231
+ reportId,
4230
4232
  completedAt,
4233
+ durationMs,
4231
4234
  "overall": summary.overall.avgScore,
4232
4235
  "scores": summary.scores[] {
4233
4236
  feature,
@@ -4237,6 +4240,10 @@ var scoreTimelineQuery = (
4237
4240
  "actualScore": summary.overall.avgActualScore,
4238
4241
  "mode": provenance.mode,
4239
4242
  "source": provenance.source.name,
4243
+ "trigger": provenance.trigger.type,
4244
+ "areas": provenance.areas,
4245
+ "models": provenance.models[].label,
4246
+ "ownerTeam": provenance.owner.team,
4240
4247
  tag,
4241
4248
  title
4242
4249
  }
@@ -12766,6 +12773,18 @@ import React4 from "react";
12766
12773
  import { Card as Card19, Stack as Stack34, Text as Text45 } from "@sanity/ui";
12767
12774
 
12768
12775
  // src/lib/scoring.ts
12776
+ var TONE_MAP = {
12777
+ good: "positive",
12778
+ warning: "caution",
12779
+ "needs-work": "default",
12780
+ critical: "critical"
12781
+ };
12782
+ function gradeTone(grade) {
12783
+ return TONE_MAP[grade];
12784
+ }
12785
+ function scoreTone(score) {
12786
+ return gradeTone(scoreGrade(Math.round(score)));
12787
+ }
12769
12788
  var HEX_MAP = {
12770
12789
  good: "#2ecc71",
12771
12790
  warning: "#f1c40f",
@@ -13358,244 +13377,1158 @@ function JudgmentDetailDrawerOutlet({
13358
13377
  }
13359
13378
 
13360
13379
  // src/components/ScoreTimeline.tsx
13361
- import { Card as Card23, Flex as Flex36, Select as Select2, Stack as Stack39, Text as Text50 } from "@sanity/ui";
13362
- import { useCallback as useCallback38, useEffect as useEffect16, useMemo as useMemo17, useState as useState30 } from "react";
13363
- import { useClient as useClient13 } from "sanity";
13364
- import { jsx as jsx66, jsxs as jsxs50 } from "react/jsx-runtime";
13365
- var CHART_HEIGHT = 220;
13366
- var CHART_WIDTH = 800;
13367
- var PAD_BOTTOM = 30;
13368
- var PAD_LEFT = 40;
13369
- var PAD_RIGHT = 20;
13370
- var PAD_TOP = 20;
13371
- var PLOT_HEIGHT = CHART_HEIGHT - PAD_TOP - PAD_BOTTOM;
13372
- var PLOT_WIDTH = CHART_WIDTH - PAD_LEFT - PAD_RIGHT;
13373
- var Y_MAX = 100;
13374
- var Y_TICKS = [0, 25, 50, 75, 100];
13375
- var TIME_RANGES = [
13376
- { days: 7, label: "Last 7 days" },
13377
- { days: 30, label: "Last 30 days" },
13378
- { days: 90, label: "Last 90 days" },
13379
- { days: null, label: "All time" }
13380
+ import { Card as Card25, Flex as Flex38, Spinner, Stack as Stack41, Text as Text52 } from "@sanity/ui";
13381
+ import { useCallback as useCallback41, useMemo as useMemo19, useState as useState32 } from "react";
13382
+ import { useRouter as useRouter4 } from "sanity/router";
13383
+
13384
+ // src/components/timeline/TimelineChart.tsx
13385
+ import { Box as Box34, Card as Card23, Flex as Flex36, Stack as Stack39, Text as Text50 } from "@sanity/ui";
13386
+ import {
13387
+ useCallback as useCallback38,
13388
+ useEffect as useEffect16,
13389
+ useMemo as useMemo17,
13390
+ useRef as useRef9,
13391
+ useState as useState30
13392
+ } from "react";
13393
+
13394
+ // src/components/timeline/lib.ts
13395
+ var RANGE_OPTIONS = [
13396
+ { days: 7, label: "Last 7 days", value: "7d" },
13397
+ { days: 30, label: "Last 30 days", value: "30d" },
13398
+ { days: 90, label: "Last 90 days", value: "90d" },
13399
+ { days: 180, label: "Last 180 days", value: "180d" },
13400
+ { days: 365, label: "Last year", value: "365d" },
13401
+ { days: null, label: "All time", value: "all" }
13402
+ ];
13403
+ var DEFAULT_RANGE = "30d";
13404
+ function rangePresetToDays(preset) {
13405
+ const opt = RANGE_OPTIONS.find((r) => r.value === preset);
13406
+ if (!opt) throw new Error(`Unknown range preset: ${preset}`);
13407
+ return opt.days;
13408
+ }
13409
+ function isRangePreset(value) {
13410
+ return RANGE_OPTIONS.some((r) => r.value === value);
13411
+ }
13412
+ var GRANULARITY_OPTIONS = [
13413
+ { label: "All", value: "raw" },
13414
+ { label: "Daily average", value: "day" },
13415
+ { label: "Weekly average", value: "week" }
13380
13416
  ];
13417
+ var DEFAULT_GRANULARITY = "raw";
13418
+ function isGranularity(value) {
13419
+ return value === "raw" || value === "day" || value === "week";
13420
+ }
13381
13421
  function daysAgo(n) {
13382
13422
  const d = /* @__PURE__ */ new Date();
13383
13423
  d.setDate(d.getDate() - n);
13384
13424
  return d.toISOString();
13385
13425
  }
13386
- function formatDate(iso) {
13426
+ function formatDateShort(iso) {
13387
13427
  return new Date(iso).toLocaleDateString(void 0, {
13388
13428
  day: "numeric",
13389
13429
  month: "short"
13390
13430
  });
13391
13431
  }
13432
+ function formatDateTime(iso) {
13433
+ return new Date(iso).toLocaleString(void 0, {
13434
+ day: "numeric",
13435
+ hour: "2-digit",
13436
+ minute: "2-digit",
13437
+ month: "short",
13438
+ year: "numeric"
13439
+ });
13440
+ }
13441
+ function startOfDay(iso) {
13442
+ const d = new Date(iso);
13443
+ d.setUTCHours(0, 0, 0, 0);
13444
+ return d.toISOString();
13445
+ }
13446
+ function startOfWeek(iso) {
13447
+ const d = new Date(iso);
13448
+ d.setUTCHours(0, 0, 0, 0);
13449
+ const day = d.getUTCDay();
13450
+ const diff = (day + 6) % 7;
13451
+ d.setUTCDate(d.getUTCDate() - diff);
13452
+ return d.toISOString();
13453
+ }
13392
13454
  function scoreForPoint(point, area) {
13393
13455
  if (!area) return point.overall;
13394
13456
  const match = point.scores.find((s) => s.feature === area);
13395
13457
  return match?.totalScore ?? null;
13396
13458
  }
13397
- function ScoreTimeline({ mode = null, source = null }) {
13398
- const client = useClient13({ apiVersion: API_VERSION });
13399
- const [dataPoints, setDataPoints] = useState30([]);
13400
- const [loading, setLoading] = useState30(true);
13401
- const [rangeDays, setRangeDays] = useState30(30);
13402
- const [selectedArea, setSelectedArea] = useState30(null);
13403
- const areaNames = useMemo17(() => {
13404
- const names = /* @__PURE__ */ new Set();
13405
- for (const dp of dataPoints) {
13406
- for (const s of dp.scores) {
13407
- names.add(s.feature);
13408
- }
13459
+ function aggregate(points, area, granularity) {
13460
+ const scored = [];
13461
+ for (const dp of points) {
13462
+ const score = scoreForPoint(dp, area);
13463
+ if (score === null) continue;
13464
+ scored.push({ count: 1, date: dp.completedAt, score, source: dp });
13465
+ }
13466
+ if (granularity === "raw" || scored.length === 0) return scored;
13467
+ const bucketKey = granularity === "day" ? startOfDay : startOfWeek;
13468
+ const buckets = /* @__PURE__ */ new Map();
13469
+ for (const d of scored) {
13470
+ const key = bucketKey(d.date);
13471
+ const cur = buckets.get(key);
13472
+ if (cur) {
13473
+ cur.sum += d.score;
13474
+ cur.count += 1;
13475
+ if (d.date > cur.source.completedAt) cur.source = d.source;
13476
+ } else {
13477
+ buckets.set(key, { count: 1, source: d.source, sum: d.score });
13409
13478
  }
13410
- return Array.from(names).sort();
13411
- }, [dataPoints]);
13412
- const fetchData = useCallback38(async () => {
13413
- setLoading(true);
13414
- try {
13415
- const startDate = rangeDays ? daysAgo(rangeDays) : "1970-01-01T00:00:00Z";
13416
- const results = await client.fetch(
13417
- scoreTimelineQuery,
13418
- { mode, source, startDate }
13419
- );
13420
- setDataPoints(results ?? []);
13421
- } catch {
13422
- setDataPoints([]);
13423
- } finally {
13424
- setLoading(false);
13479
+ }
13480
+ return Array.from(buckets.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([date, { count, source, sum }]) => ({
13481
+ count,
13482
+ date,
13483
+ score: sum / count,
13484
+ source
13485
+ }));
13486
+ }
13487
+ function uniqueSorted(items, extract) {
13488
+ const set2 = /* @__PURE__ */ new Set();
13489
+ for (const item of items) {
13490
+ for (const v of extract(item)) {
13491
+ if (v) set2.add(v);
13425
13492
  }
13426
- }, [client, mode, rangeDays, source]);
13493
+ }
13494
+ return Array.from(set2).sort();
13495
+ }
13496
+ function computeStats(points, window2 = 5) {
13497
+ if (points.length === 0) {
13498
+ return {
13499
+ count: 0,
13500
+ delta: null,
13501
+ first: null,
13502
+ last: null,
13503
+ max: null,
13504
+ mean: null,
13505
+ median: null,
13506
+ min: null,
13507
+ movingAverage: [],
13508
+ stdev: null,
13509
+ trend: "flat"
13510
+ };
13511
+ }
13512
+ const scores = points.map((p) => p.score);
13513
+ const sum = scores.reduce((a, b) => a + b, 0);
13514
+ const mean = sum / scores.length;
13515
+ const sorted = [...scores].sort((a, b) => a - b);
13516
+ const median2 = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[(sorted.length - 1) / 2];
13517
+ let minIdx = 0;
13518
+ let maxIdx = 0;
13519
+ for (let i = 1; i < points.length; i++) {
13520
+ if (points[i].score < points[minIdx].score) minIdx = i;
13521
+ if (points[i].score > points[maxIdx].score) maxIdx = i;
13522
+ }
13523
+ const variance = scores.reduce((acc, x) => acc + (x - mean) ** 2, 0) / scores.length;
13524
+ const stdev = Math.sqrt(variance);
13525
+ const first = points[0];
13526
+ const last = points[points.length - 1];
13527
+ const delta = points.length > 1 ? last.score - first.score : 0;
13528
+ const trend = classifyTrend(delta);
13529
+ return {
13530
+ count: points.length,
13531
+ delta,
13532
+ first,
13533
+ last,
13534
+ max: points[maxIdx],
13535
+ mean,
13536
+ median: median2,
13537
+ min: points[minIdx],
13538
+ movingAverage: rollingMean(scores, window2),
13539
+ stdev,
13540
+ trend
13541
+ };
13542
+ }
13543
+ function classifyTrend(delta) {
13544
+ if (Math.abs(delta) < 1.5) return "flat";
13545
+ return delta > 0 ? "up" : "down";
13546
+ }
13547
+ function rollingMean(values, window2) {
13548
+ if (values.length === 0) return [];
13549
+ const w = Math.max(1, Math.min(window2, values.length));
13550
+ const out = [];
13551
+ let runningSum = 0;
13552
+ const queue = [];
13553
+ for (const v of values) {
13554
+ queue.push(v);
13555
+ runningSum += v;
13556
+ if (queue.length > w) {
13557
+ runningSum -= queue.shift();
13558
+ }
13559
+ out.push(runningSum / queue.length);
13560
+ }
13561
+ return out;
13562
+ }
13563
+ var CSV_HEADER = [
13564
+ "completedAt",
13565
+ "reportId",
13566
+ "title",
13567
+ "tag",
13568
+ "mode",
13569
+ "source",
13570
+ "trigger",
13571
+ "overall",
13572
+ "selectedAreaScore",
13573
+ "actualScore",
13574
+ "durationMs"
13575
+ ];
13576
+ function csvEscape(value) {
13577
+ if (value == null) return "";
13578
+ const s = String(value);
13579
+ if (/[",\n\r]/.test(s)) return `"${s.replaceAll('"', '""')}"`;
13580
+ return s;
13581
+ }
13582
+ function toCsv(points, selectedArea) {
13583
+ const rows = [CSV_HEADER.join(",")];
13584
+ for (const p of points) {
13585
+ const areaScore = scoreForPoint(p, selectedArea);
13586
+ rows.push(
13587
+ [
13588
+ p.completedAt,
13589
+ p.reportId ?? p._id,
13590
+ p.title ?? "",
13591
+ p.tag ?? "",
13592
+ p.mode,
13593
+ p.source,
13594
+ p.trigger ?? "",
13595
+ p.overall ?? "",
13596
+ areaScore ?? "",
13597
+ p.actualScore ?? "",
13598
+ p.durationMs ?? ""
13599
+ ].map(csvEscape).join(",")
13600
+ );
13601
+ }
13602
+ return rows.join("\n") + "\n";
13603
+ }
13604
+ function downloadCsv(content, filename) {
13605
+ if (typeof document === "undefined") return;
13606
+ const blob = new Blob([content], { type: "text/csv;charset=utf-8" });
13607
+ const url = URL.createObjectURL(blob);
13608
+ const link = document.createElement("a");
13609
+ link.href = url;
13610
+ link.download = filename;
13611
+ document.body.append(link);
13612
+ link.click();
13613
+ link.remove();
13614
+ setTimeout(() => URL.revokeObjectURL(url), 0);
13615
+ }
13616
+ async function copyTextToClipboard(text) {
13617
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
13618
+ return false;
13619
+ }
13620
+ try {
13621
+ await navigator.clipboard.writeText(text);
13622
+ return true;
13623
+ } catch {
13624
+ return false;
13625
+ }
13626
+ }
13627
+ var TIMELINE_DATA_START_DATE = "2026-04-15T00:00:00Z";
13628
+ function effectiveStartDate(rangeDays, floor = TIMELINE_DATA_START_DATE) {
13629
+ const rangeStart = rangeDays ? daysAgo(rangeDays) : "1970-01-01T00:00:00Z";
13630
+ if (!floor) return rangeStart;
13631
+ return rangeStart > floor ? rangeStart : floor;
13632
+ }
13633
+
13634
+ // src/components/timeline/TimelineChart.tsx
13635
+ import { jsx as jsx66, jsxs as jsxs50 } from "react/jsx-runtime";
13636
+ var CHART_HEIGHT = 280;
13637
+ var PAD_BOTTOM = 36;
13638
+ var PAD_LEFT = 44;
13639
+ var PAD_RIGHT = 16;
13640
+ var PAD_TOP = 16;
13641
+ var Y_MAX = 100;
13642
+ var Y_TICKS = [0, 25, 50, 75, 100];
13643
+ var NOISE_BAND = 3;
13644
+ var X_LABEL_TARGET = 6;
13645
+ function TimelineChart({
13646
+ movingAverage,
13647
+ onSelectPoint,
13648
+ points,
13649
+ showMovingAverage
13650
+ }) {
13651
+ const containerRef = useRef9(null);
13652
+ const [width, setWidth] = useState30(800);
13653
+ const [hoverIdx, setHoverIdx] = useState30(null);
13654
+ const [focusIdx, setFocusIdx] = useState30(null);
13427
13655
  useEffect16(() => {
13428
- void fetchData();
13429
- }, [fetchData]);
13430
- const chartPoints = useMemo17(() => {
13431
- const pts = [];
13432
- const scored = dataPoints.map((dp) => ({
13433
- date: dp.completedAt,
13434
- score: scoreForPoint(dp, selectedArea)
13435
- })).filter((d) => d.score !== null);
13436
- if (scored.length === 0) return pts;
13437
- scored.forEach((d, i) => {
13438
- const x = PAD_LEFT + (scored.length === 1 ? PLOT_WIDTH / 2 : i / (scored.length - 1) * PLOT_WIDTH);
13439
- const y = PAD_TOP + PLOT_HEIGHT - d.score / Y_MAX * PLOT_HEIGHT;
13440
- pts.push({ date: d.date, score: d.score, x, y });
13656
+ const node = containerRef.current;
13657
+ if (!node || typeof ResizeObserver === "undefined") return;
13658
+ const ro = new ResizeObserver((entries) => {
13659
+ const entry = entries[0];
13660
+ if (!entry) return;
13661
+ const w = Math.max(320, Math.floor(entry.contentRect.width));
13662
+ setWidth(w);
13441
13663
  });
13442
- return pts;
13443
- }, [dataPoints, selectedArea]);
13664
+ ro.observe(node);
13665
+ return () => ro.disconnect();
13666
+ }, []);
13667
+ const plotWidth = width - PAD_LEFT - PAD_RIGHT;
13668
+ const plotHeight = CHART_HEIGHT - PAD_TOP - PAD_BOTTOM;
13669
+ const xFor = useCallback38(
13670
+ (i, n) => PAD_LEFT + (n === 1 ? plotWidth / 2 : i / (n - 1) * plotWidth),
13671
+ [plotWidth]
13672
+ );
13673
+ const yFor = useCallback38(
13674
+ (score) => PAD_TOP + plotHeight - score / Y_MAX * plotHeight,
13675
+ [plotHeight]
13676
+ );
13677
+ const computed = useMemo17(
13678
+ () => points.map((p, i) => ({
13679
+ ...p,
13680
+ x: xFor(i, points.length),
13681
+ y: yFor(p.score)
13682
+ })),
13683
+ [points, xFor, yFor]
13684
+ );
13685
+ const polylinePoints = useMemo17(
13686
+ () => computed.map((p) => `${p.x},${p.y}`).join(" "),
13687
+ [computed]
13688
+ );
13689
+ const maPoints = useMemo17(() => {
13690
+ if (!showMovingAverage || movingAverage.length !== computed.length)
13691
+ return "";
13692
+ return computed.map((p, i) => `${p.x},${yFor(movingAverage[i] ?? p.score)}`).join(" ");
13693
+ }, [computed, movingAverage, showMovingAverage, yFor]);
13444
13694
  const avgScore = useMemo17(() => {
13445
- if (chartPoints.length === 0) return 0;
13446
- return chartPoints.reduce((sum, p) => sum + p.score, 0) / chartPoints.length;
13447
- }, [chartPoints]);
13448
- const handleRangeChange = useCallback38(
13695
+ if (computed.length === 0) return 0;
13696
+ return computed.reduce((acc, p) => acc + p.score, 0) / computed.length;
13697
+ }, [computed]);
13698
+ const xLabelIndexes = useMemo17(() => {
13699
+ if (computed.length === 0) return [];
13700
+ if (computed.length <= X_LABEL_TARGET) return computed.map((_, i) => i);
13701
+ const step = (computed.length - 1) / (X_LABEL_TARGET - 1);
13702
+ const out = [];
13703
+ for (let k = 0; k < X_LABEL_TARGET; k++) {
13704
+ out.push(Math.round(k * step));
13705
+ }
13706
+ return Array.from(new Set(out));
13707
+ }, [computed]);
13708
+ const findNearestIndex = useCallback38(
13709
+ (clientX, rect) => {
13710
+ if (computed.length === 0) return null;
13711
+ const localX = (clientX - rect.left) * width / rect.width;
13712
+ let best = 0;
13713
+ let bestDist = Math.abs(computed[0].x - localX);
13714
+ for (let i = 1; i < computed.length; i++) {
13715
+ const d = Math.abs(computed[i].x - localX);
13716
+ if (d < bestDist) {
13717
+ best = i;
13718
+ bestDist = d;
13719
+ }
13720
+ }
13721
+ return best;
13722
+ },
13723
+ [computed, width]
13724
+ );
13725
+ const handleMouseMove = useCallback38(
13449
13726
  (e) => {
13450
- const val = e.currentTarget.value;
13451
- setRangeDays(val === "all" ? null : Number(val));
13727
+ const rect = e.currentTarget.getBoundingClientRect();
13728
+ const idx = findNearestIndex(e.clientX, rect);
13729
+ setHoverIdx(idx);
13452
13730
  },
13453
- []
13731
+ [findNearestIndex]
13732
+ );
13733
+ const handleMouseLeave = useCallback38(() => {
13734
+ setHoverIdx(null);
13735
+ }, []);
13736
+ const handlePointClick = useCallback38(
13737
+ (idx) => {
13738
+ const datum = computed[idx];
13739
+ if (!datum) return;
13740
+ onSelectPoint(datum.source);
13741
+ },
13742
+ [computed, onSelectPoint]
13454
13743
  );
13455
- const handleAreaChange = useCallback38(
13744
+ const handleFocus = useCallback38(() => {
13745
+ setFocusIdx((prev) => prev ?? 0);
13746
+ }, []);
13747
+ const moveTo = useCallback38((idx) => {
13748
+ setFocusIdx(idx);
13749
+ setHoverIdx(idx);
13750
+ }, []);
13751
+ const handleKey = useCallback38(
13456
13752
  (e) => {
13457
- const val = e.currentTarget.value;
13458
- setSelectedArea(val || null);
13753
+ if (computed.length === 0) return;
13754
+ const last = computed.length - 1;
13755
+ const cur = focusIdx ?? 0;
13756
+ switch (e.key) {
13757
+ case "ArrowRight":
13758
+ e.preventDefault();
13759
+ moveTo(Math.min(last, cur + 1));
13760
+ return;
13761
+ case "ArrowLeft":
13762
+ e.preventDefault();
13763
+ moveTo(Math.max(0, cur - 1));
13764
+ return;
13765
+ case "Home":
13766
+ e.preventDefault();
13767
+ moveTo(0);
13768
+ return;
13769
+ case "End":
13770
+ e.preventDefault();
13771
+ moveTo(last);
13772
+ return;
13773
+ case "Enter":
13774
+ case " ":
13775
+ e.preventDefault();
13776
+ handlePointClick(cur);
13777
+ }
13459
13778
  },
13460
- []
13779
+ [computed, focusIdx, handlePointClick, moveTo]
13461
13780
  );
13462
- const polylinePoints = chartPoints.map((p) => `${p.x},${p.y}`).join(" ");
13463
- return /* @__PURE__ */ jsxs50(Stack39, { space: 4, children: [
13464
- /* @__PURE__ */ jsxs50(Flex36, { gap: 3, children: [
13465
- /* @__PURE__ */ jsx66(
13466
- Select2,
13781
+ const activeIdx = hoverIdx ?? focusIdx;
13782
+ const active = activeIdx !== null ? computed[activeIdx] : null;
13783
+ if (computed.length === 0) {
13784
+ return /* @__PURE__ */ jsx66(Card23, { padding: 4, radius: 2, shadow: 1, children: /* @__PURE__ */ jsx66(Flex36, { align: "center", justify: "center", style: { height: 220 }, children: /* @__PURE__ */ jsx66(Text50, { muted: true, size: 2, children: "No reports found in this window. Adjust the filters or extend the time range." }) }) });
13785
+ }
13786
+ return /* @__PURE__ */ jsx66(Card23, { padding: 3, radius: 2, shadow: 1, children: /* @__PURE__ */ jsxs50(Stack39, { space: 2, children: [
13787
+ /* @__PURE__ */ jsxs50(Box34, { ref: containerRef, style: { position: "relative", width: "100%" }, children: [
13788
+ /* @__PURE__ */ jsxs50(
13789
+ "svg",
13467
13790
  {
13468
- onChange: handleRangeChange,
13469
- value: rangeDays?.toString() ?? "all",
13470
- children: TIME_RANGES.map((r) => /* @__PURE__ */ jsx66("option", { value: r.days?.toString() ?? "all", children: r.label }, r.label))
13791
+ "aria-label": "Score timeline chart",
13792
+ onFocus: handleFocus,
13793
+ onKeyDown: handleKey,
13794
+ onMouseLeave: handleMouseLeave,
13795
+ onMouseMove: handleMouseMove,
13796
+ role: "img",
13797
+ style: { display: "block", width: "100%" },
13798
+ tabIndex: 0,
13799
+ viewBox: `0 0 ${width} ${CHART_HEIGHT}`,
13800
+ children: [
13801
+ Y_TICKS.map((tick) => {
13802
+ const y = yFor(tick);
13803
+ return /* @__PURE__ */ jsxs50("g", { children: [
13804
+ /* @__PURE__ */ jsx66(
13805
+ "line",
13806
+ {
13807
+ stroke: "var(--card-border-color, #ddd)",
13808
+ strokeDasharray: "3 4",
13809
+ x1: PAD_LEFT,
13810
+ x2: width - PAD_RIGHT,
13811
+ y1: y,
13812
+ y2: y
13813
+ }
13814
+ ),
13815
+ /* @__PURE__ */ jsx66(
13816
+ "text",
13817
+ {
13818
+ dominantBaseline: "middle",
13819
+ fill: "var(--card-muted-fg-color, #999)",
13820
+ fontSize: 11,
13821
+ textAnchor: "end",
13822
+ x: PAD_LEFT - 6,
13823
+ y,
13824
+ children: tick
13825
+ }
13826
+ )
13827
+ ] }, tick);
13828
+ }),
13829
+ avgScore > 0 ? /* @__PURE__ */ jsx66(
13830
+ "rect",
13831
+ {
13832
+ fill: scoreHex(avgScore),
13833
+ fillOpacity: 0.07,
13834
+ height: Math.max(
13835
+ 0,
13836
+ yFor(avgScore - NOISE_BAND) - yFor(avgScore + NOISE_BAND)
13837
+ ),
13838
+ width: width - PAD_LEFT - PAD_RIGHT,
13839
+ x: PAD_LEFT,
13840
+ y: yFor(avgScore + NOISE_BAND)
13841
+ }
13842
+ ) : null,
13843
+ xLabelIndexes.map((i) => {
13844
+ const p = computed[i];
13845
+ if (!p) return null;
13846
+ return /* @__PURE__ */ jsx66(
13847
+ "text",
13848
+ {
13849
+ fill: "var(--card-muted-fg-color, #999)",
13850
+ fontSize: 10,
13851
+ textAnchor: i === 0 ? "start" : i === computed.length - 1 ? "end" : "middle",
13852
+ x: p.x,
13853
+ y: CHART_HEIGHT - 10,
13854
+ children: formatDateShort(p.date)
13855
+ },
13856
+ i
13857
+ );
13858
+ }),
13859
+ showMovingAverage && maPoints ? /* @__PURE__ */ jsx66(
13860
+ "polyline",
13861
+ {
13862
+ fill: "none",
13863
+ points: maPoints,
13864
+ stroke: "var(--card-muted-fg-color, #888)",
13865
+ strokeDasharray: "6 4",
13866
+ strokeLinejoin: "round",
13867
+ strokeWidth: 1.5
13868
+ }
13869
+ ) : null,
13870
+ /* @__PURE__ */ jsx66(
13871
+ "polyline",
13872
+ {
13873
+ fill: "none",
13874
+ points: polylinePoints,
13875
+ stroke: scoreHex(avgScore),
13876
+ strokeLinejoin: "round",
13877
+ strokeWidth: 2.5
13878
+ }
13879
+ ),
13880
+ active ? /* @__PURE__ */ jsx66(
13881
+ "line",
13882
+ {
13883
+ stroke: "var(--card-border-color, #bbb)",
13884
+ strokeWidth: 1,
13885
+ x1: active.x,
13886
+ x2: active.x,
13887
+ y1: PAD_TOP,
13888
+ y2: CHART_HEIGHT - PAD_BOTTOM
13889
+ }
13890
+ ) : null,
13891
+ computed.map((p, idx) => {
13892
+ const isActive = idx === activeIdx;
13893
+ return /* @__PURE__ */ jsx66(
13894
+ "circle",
13895
+ {
13896
+ cx: p.x,
13897
+ cy: p.y,
13898
+ fill: scoreHex(p.score),
13899
+ onClick: () => handlePointClick(idx),
13900
+ r: isActive ? 6 : 4,
13901
+ stroke: "var(--card-bg-color, #fff)",
13902
+ strokeWidth: isActive ? 2 : 1.5,
13903
+ style: { cursor: "pointer" }
13904
+ },
13905
+ idx
13906
+ );
13907
+ })
13908
+ ]
13471
13909
  }
13472
13910
  ),
13473
- /* @__PURE__ */ jsxs50(Select2, { onChange: handleAreaChange, value: selectedArea ?? "", children: [
13474
- /* @__PURE__ */ jsx66("option", { value: "", children: "Overall" }),
13475
- areaNames.map((name) => /* @__PURE__ */ jsx66("option", { value: name, children: name }, name))
13476
- ] })
13911
+ active ? /* @__PURE__ */ jsx66(Tooltip13, { active, chartWidth: width }) : null
13477
13912
  ] }),
13478
- /* @__PURE__ */ jsx66(Card23, { padding: 3, radius: 2, shadow: 1, children: loading ? /* @__PURE__ */ jsx66(Flex36, { align: "center", justify: "center", style: { height: 200 }, children: /* @__PURE__ */ jsx66(Text50, { muted: true, size: 2, children: "Loading\u2026" }) }) : chartPoints.length === 0 ? /* @__PURE__ */ jsx66(Flex36, { align: "center", justify: "center", style: { height: 200 }, children: /* @__PURE__ */ jsx66(Text50, { muted: true, size: 2, children: "No reports found for this time range" }) }) : /* @__PURE__ */ jsxs50(
13479
- "svg",
13913
+ /* @__PURE__ */ jsxs50(Flex36, { gap: 3, align: "center", children: [
13914
+ /* @__PURE__ */ jsx66(LegendDot, { color: scoreHex(avgScore), label: "Score" }),
13915
+ showMovingAverage ? /* @__PURE__ */ jsx66(
13916
+ LegendDot,
13917
+ {
13918
+ color: "var(--card-muted-fg-color, #888)",
13919
+ dashed: true,
13920
+ label: "5-point moving average"
13921
+ }
13922
+ ) : null,
13923
+ /* @__PURE__ */ jsx66(Text50, { muted: true, size: 1, children: "Click a point to open the report" })
13924
+ ] })
13925
+ ] }) });
13926
+ }
13927
+ function LegendDot({
13928
+ color,
13929
+ dashed = false,
13930
+ label
13931
+ }) {
13932
+ return /* @__PURE__ */ jsxs50(Flex36, { align: "center", gap: 2, children: [
13933
+ /* @__PURE__ */ jsx66("svg", { height: "10", width: "22", children: /* @__PURE__ */ jsx66(
13934
+ "line",
13480
13935
  {
13481
- style: { display: "block", width: "100%" },
13482
- viewBox: `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`,
13483
- children: [
13484
- Y_TICKS.map((tick) => {
13485
- const y = PAD_TOP + PLOT_HEIGHT - tick / Y_MAX * PLOT_HEIGHT;
13486
- return /* @__PURE__ */ jsxs50("g", { children: [
13487
- /* @__PURE__ */ jsx66(
13488
- "line",
13489
- {
13490
- stroke: "#ccc",
13491
- strokeDasharray: "4 4",
13492
- x1: PAD_LEFT,
13493
- x2: CHART_WIDTH - PAD_RIGHT,
13494
- y1: y,
13495
- y2: y
13496
- }
13497
- ),
13498
- /* @__PURE__ */ jsx66(
13499
- "text",
13500
- {
13501
- dominantBaseline: "middle",
13502
- fill: "#999",
13503
- fontSize: 11,
13504
- textAnchor: "end",
13505
- x: PAD_LEFT - 6,
13506
- y,
13507
- children: tick
13508
- }
13509
- )
13510
- ] }, tick);
13511
- }),
13512
- chartPoints.length >= 3 ? [
13513
- 0,
13514
- Math.floor(chartPoints.length / 2),
13515
- chartPoints.length - 1
13516
- ].map((idx) => {
13517
- const p = chartPoints[idx];
13518
- return /* @__PURE__ */ jsx66(
13519
- "text",
13520
- {
13521
- fill: "#999",
13522
- fontSize: 10,
13523
- textAnchor: "middle",
13524
- x: p.x,
13525
- y: CHART_HEIGHT - 6,
13526
- children: formatDate(p.date)
13527
- },
13528
- idx
13529
- );
13530
- }) : chartPoints.map((p, idx) => /* @__PURE__ */ jsx66(
13531
- "text",
13532
- {
13533
- fill: "#999",
13534
- fontSize: 10,
13535
- textAnchor: "middle",
13536
- x: p.x,
13537
- y: CHART_HEIGHT - 6,
13538
- children: formatDate(p.date)
13539
- },
13540
- idx
13541
- )),
13936
+ stroke: color,
13937
+ strokeDasharray: dashed ? "4 3" : void 0,
13938
+ strokeWidth: 2.5,
13939
+ x1: "0",
13940
+ x2: "22",
13941
+ y1: "5",
13942
+ y2: "5"
13943
+ }
13944
+ ) }),
13945
+ /* @__PURE__ */ jsx66(Text50, { muted: true, size: 1, children: label })
13946
+ ] });
13947
+ }
13948
+ function Tooltip13({ active, chartWidth }) {
13949
+ const cssX = active.x;
13950
+ const isRightEdge = cssX > chartWidth - 220;
13951
+ const left = isRightEdge ? cssX - 220 : cssX + 12;
13952
+ return /* @__PURE__ */ jsx66(
13953
+ Box34,
13954
+ {
13955
+ style: {
13956
+ background: "var(--card-bg-color, #fff)",
13957
+ border: "1px solid var(--card-border-color, #ddd)",
13958
+ borderRadius: 4,
13959
+ boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
13960
+ left,
13961
+ maxWidth: 240,
13962
+ minWidth: 180,
13963
+ padding: 8,
13964
+ pointerEvents: "none",
13965
+ position: "absolute",
13966
+ top: 8
13967
+ },
13968
+ children: /* @__PURE__ */ jsxs50(Stack39, { space: 2, children: [
13969
+ /* @__PURE__ */ jsx66(Text50, { size: 1, weight: "semibold", children: formatDateTime(active.date) }),
13970
+ /* @__PURE__ */ jsxs50(Flex36, { gap: 2, align: "center", children: [
13542
13971
  /* @__PURE__ */ jsx66(
13543
- "polyline",
13972
+ Box34,
13544
13973
  {
13545
- fill: "none",
13546
- points: polylinePoints,
13547
- stroke: scoreHex(avgScore),
13548
- strokeLinejoin: "round",
13549
- strokeWidth: 2.5
13974
+ style: {
13975
+ background: scoreHex(active.score),
13976
+ borderRadius: "50%",
13977
+ height: 10,
13978
+ width: 10
13979
+ }
13550
13980
  }
13551
13981
  ),
13552
- chartPoints.map((p, idx) => /* @__PURE__ */ jsx66(
13553
- "circle",
13554
- {
13555
- cx: p.x,
13556
- cy: p.y,
13557
- fill: scoreHex(p.score),
13558
- r: 4,
13559
- stroke: "#fff",
13560
- strokeWidth: 1.5,
13561
- children: /* @__PURE__ */ jsxs50("title", { children: [
13562
- formatDate(p.date),
13563
- ": ",
13564
- Math.round(p.score)
13565
- ] })
13566
- },
13567
- idx
13568
- ))
13569
- ]
13982
+ /* @__PURE__ */ jsx66(Text50, { size: 2, weight: "bold", children: Math.round(active.score) }),
13983
+ active.count > 1 ? /* @__PURE__ */ jsxs50(Text50, { muted: true, size: 1, children: [
13984
+ "avg of ",
13985
+ active.count,
13986
+ " reports"
13987
+ ] }) : null
13988
+ ] }),
13989
+ active.source.title ? /* @__PURE__ */ jsx66(Text50, { size: 1, children: active.source.title }) : null,
13990
+ /* @__PURE__ */ jsxs50(Text50, { muted: true, size: 1, children: [
13991
+ active.source.mode,
13992
+ active.source.source ? ` \xB7 ${active.source.source}` : ""
13993
+ ] }),
13994
+ active.source.tag ? /* @__PURE__ */ jsxs50(Text50, { muted: true, size: 1, children: [
13995
+ "tag: ",
13996
+ active.source.tag
13997
+ ] }) : null
13998
+ ] })
13999
+ }
14000
+ );
14001
+ }
14002
+
14003
+ // src/components/timeline/TimelineFilters.tsx
14004
+ import {
14005
+ ClipboardIcon as ClipboardIcon2,
14006
+ DownloadIcon as DownloadIcon2,
14007
+ LinkIcon as LinkIcon4,
14008
+ RefreshIcon
14009
+ } from "@sanity/icons";
14010
+ import { Box as Box35, Button as Button14, Inline as Inline3, MenuDivider as MenuDivider4, MenuItem as MenuItem10 } from "@sanity/ui";
14011
+ import { useCallback as useCallback39 } from "react";
14012
+
14013
+ // src/components/timeline/SelectChip.tsx
14014
+ import { ChevronDownIcon as ChevronDownIcon5 } from "@sanity/icons";
14015
+ import { Button as Button13, Menu as Menu3, MenuButton as MenuButton3, MenuItem as MenuItem9 } from "@sanity/ui";
14016
+ import { useId as useId2 } from "react";
14017
+ import { jsx as jsx67 } from "react/jsx-runtime";
14018
+ function SelectChip({
14019
+ defaultValue,
14020
+ label,
14021
+ onChange,
14022
+ options,
14023
+ value
14024
+ }) {
14025
+ const menuId = useId2();
14026
+ const current = options.find((o) => o.value === value);
14027
+ const displayValue = current?.label ?? value;
14028
+ const isActive = defaultValue !== void 0 && value !== defaultValue;
14029
+ return /* @__PURE__ */ jsx67(
14030
+ MenuButton3,
14031
+ {
14032
+ button: /* @__PURE__ */ jsx67(
14033
+ Button13,
14034
+ {
14035
+ fontSize: 1,
14036
+ iconRight: ChevronDownIcon5,
14037
+ mode: isActive ? "default" : "ghost",
14038
+ padding: 2,
14039
+ radius: 6,
14040
+ text: `${label}: ${displayValue}`,
14041
+ tone: isActive ? "primary" : "default"
14042
+ }
14043
+ ),
14044
+ id: menuId,
14045
+ menu: /* @__PURE__ */ jsx67(Menu3, { style: { minWidth: 160 }, children: options.map((opt) => /* @__PURE__ */ jsx67(
14046
+ MenuItem9,
14047
+ {
14048
+ onClick: () => onChange(opt.value),
14049
+ selected: value === opt.value,
14050
+ text: opt.label
14051
+ },
14052
+ opt.value
14053
+ )) }),
14054
+ popover: {
14055
+ animate: true,
14056
+ constrainSize: true,
14057
+ placement: "bottom-start",
14058
+ portal: true
13570
14059
  }
13571
- ) }),
13572
- /* @__PURE__ */ jsxs50(Text50, { muted: true, size: 2, children: [
13573
- chartPoints.length,
13574
- " data point",
13575
- chartPoints.length !== 1 ? "s" : ""
14060
+ }
14061
+ );
14062
+ }
14063
+
14064
+ // src/components/timeline/TimelineFilters.tsx
14065
+ import { Fragment as Fragment15, jsx as jsx68, jsxs as jsxs51 } from "react/jsx-runtime";
14066
+ var BAR_STYLE2 = {
14067
+ alignItems: "center",
14068
+ background: "var(--card-bg-color)",
14069
+ border: "1px solid var(--card-border-color, transparent)",
14070
+ borderRadius: 4,
14071
+ display: "flex",
14072
+ flexWrap: "wrap",
14073
+ gap: 8,
14074
+ padding: "8px 12px"
14075
+ };
14076
+ function TimelineFilters(props) {
14077
+ const {
14078
+ area,
14079
+ areaOptions,
14080
+ granularity,
14081
+ loading,
14082
+ mode,
14083
+ modeOptions,
14084
+ onAreaChange,
14085
+ onCopyCsv,
14086
+ onCopyUrl,
14087
+ onExportCsv,
14088
+ onGranularityChange,
14089
+ onModeChange,
14090
+ onOwnerTeamChange,
14091
+ onRangeChange,
14092
+ onRefresh,
14093
+ onShowMovingAverageChange,
14094
+ onSourceChange,
14095
+ ownerTeam,
14096
+ ownerTeamOptions,
14097
+ range,
14098
+ showMovingAverage,
14099
+ source,
14100
+ sourceOptions
14101
+ } = props;
14102
+ const handleMaToggle = useCallback39(() => {
14103
+ onShowMovingAverageChange(!showMovingAverage);
14104
+ }, [onShowMovingAverageChange, showMovingAverage]);
14105
+ return /* @__PURE__ */ jsxs51("div", { style: BAR_STYLE2, children: [
14106
+ /* @__PURE__ */ jsx68(
14107
+ SelectChip,
14108
+ {
14109
+ defaultValue: DEFAULT_RANGE,
14110
+ label: "Range",
14111
+ onChange: onRangeChange,
14112
+ options: RANGE_OPTIONS,
14113
+ value: range
14114
+ }
14115
+ ),
14116
+ /* @__PURE__ */ jsx68(
14117
+ SelectChip,
14118
+ {
14119
+ defaultValue: DEFAULT_GRANULARITY,
14120
+ label: "Aggregate",
14121
+ onChange: onGranularityChange,
14122
+ options: GRANULARITY_OPTIONS,
14123
+ value: granularity
14124
+ }
14125
+ ),
14126
+ areaOptions.length > 0 && /* @__PURE__ */ jsx68(
14127
+ FilterChip,
14128
+ {
14129
+ label: "Area",
14130
+ onChange: onAreaChange,
14131
+ options: areaOptions,
14132
+ searchable: true,
14133
+ value: area
14134
+ }
14135
+ ),
14136
+ modeOptions.length > 0 && /* @__PURE__ */ jsx68(
14137
+ FilterChip,
14138
+ {
14139
+ label: "Mode",
14140
+ onChange: onModeChange,
14141
+ options: modeOptions,
14142
+ value: mode
14143
+ }
14144
+ ),
14145
+ sourceOptions.length > 0 && /* @__PURE__ */ jsx68(
14146
+ FilterChip,
14147
+ {
14148
+ label: "Source",
14149
+ onChange: onSourceChange,
14150
+ options: sourceOptions,
14151
+ value: source
14152
+ }
14153
+ ),
14154
+ ownerTeamOptions.length > 0 && /* @__PURE__ */ jsx68(
14155
+ FilterChip,
14156
+ {
14157
+ label: "Team",
14158
+ onChange: onOwnerTeamChange,
14159
+ options: ownerTeamOptions,
14160
+ searchable: true,
14161
+ value: ownerTeam
14162
+ }
14163
+ ),
14164
+ /* @__PURE__ */ jsx68(Box35, { style: { flex: "1 0 0px" } }),
14165
+ /* @__PURE__ */ jsxs51(Inline3, { space: 1, children: [
14166
+ /* @__PURE__ */ jsx68(
14167
+ Button14,
14168
+ {
14169
+ fontSize: 1,
14170
+ mode: showMovingAverage ? "default" : "ghost",
14171
+ onClick: handleMaToggle,
14172
+ padding: 2,
14173
+ radius: 6,
14174
+ text: "5-pt MA",
14175
+ tone: showMovingAverage ? "primary" : "default"
14176
+ }
14177
+ ),
14178
+ /* @__PURE__ */ jsx68(
14179
+ Button14,
14180
+ {
14181
+ fontSize: 1,
14182
+ icon: RefreshIcon,
14183
+ mode: "ghost",
14184
+ onClick: onRefresh,
14185
+ disabled: loading,
14186
+ padding: 2,
14187
+ radius: 6,
14188
+ text: "Refresh"
14189
+ }
14190
+ ),
14191
+ /* @__PURE__ */ jsx68(
14192
+ SplitActionButton,
14193
+ {
14194
+ menuId: "timeline-share-actions",
14195
+ menu: /* @__PURE__ */ jsxs51(Fragment15, { children: [
14196
+ /* @__PURE__ */ jsx68(
14197
+ MenuItem10,
14198
+ {
14199
+ icon: ClipboardIcon2,
14200
+ onClick: onCopyCsv,
14201
+ text: "Copy CSV to clipboard"
14202
+ }
14203
+ ),
14204
+ /* @__PURE__ */ jsx68(MenuDivider4, {}),
14205
+ /* @__PURE__ */ jsx68(
14206
+ MenuItem10,
14207
+ {
14208
+ icon: DownloadIcon2,
14209
+ onClick: onExportCsv,
14210
+ text: "Export CSV"
14211
+ }
14212
+ )
14213
+ ] }),
14214
+ primary: {
14215
+ fontSize: 1,
14216
+ icon: LinkIcon4,
14217
+ onClick: onCopyUrl,
14218
+ padding: 2,
14219
+ text: "Copy URL"
14220
+ }
14221
+ }
14222
+ )
14223
+ ] })
14224
+ ] });
14225
+ }
14226
+
14227
+ // src/components/timeline/TimelineSummary.tsx
14228
+ import { Badge as Badge13, Box as Box36, Card as Card24, Flex as Flex37, Grid as Grid7, Stack as Stack40, Text as Text51 } from "@sanity/ui";
14229
+ import { jsx as jsx69, jsxs as jsxs52 } from "react/jsx-runtime";
14230
+ function TimelineSummary({ area, stats }) {
14231
+ if (stats.count === 0) return null;
14232
+ return /* @__PURE__ */ jsx69(Card24, { padding: 3, radius: 2, shadow: 1, children: /* @__PURE__ */ jsxs52(Stack40, { space: 3, children: [
14233
+ /* @__PURE__ */ jsxs52(Flex37, { align: "center", gap: 2, children: [
14234
+ /* @__PURE__ */ jsx69(Text51, { size: 1, weight: "semibold", children: "Summary" }),
14235
+ /* @__PURE__ */ jsxs52(Text51, { muted: true, size: 1, children: [
14236
+ area ? `Area: ${area}` : "Overall score",
14237
+ " \xB7 ",
14238
+ stats.count,
14239
+ " report",
14240
+ stats.count === 1 ? "" : "s"
14241
+ ] }),
14242
+ /* @__PURE__ */ jsx69(Box36, { flex: 1 }),
14243
+ /* @__PURE__ */ jsx69(TrendPill, { stats })
14244
+ ] }),
14245
+ /* @__PURE__ */ jsxs52(Grid7, { columns: [2, 2, 4], gap: 3, children: [
14246
+ /* @__PURE__ */ jsx69(
14247
+ StatTile,
14248
+ {
14249
+ label: "Mean",
14250
+ score: stats.mean,
14251
+ sub: stats.stdev !== null ? `\xB1 ${stats.stdev.toFixed(1)} stdev` : null
14252
+ }
14253
+ ),
14254
+ /* @__PURE__ */ jsx69(StatTile, { label: "Median", score: stats.median, sub: null }),
14255
+ /* @__PURE__ */ jsx69(
14256
+ StatTile,
14257
+ {
14258
+ label: "Best",
14259
+ score: stats.max?.score ?? null,
14260
+ sub: stats.max ? formatDateShort(stats.max.date) : null
14261
+ }
14262
+ ),
14263
+ /* @__PURE__ */ jsx69(
14264
+ StatTile,
14265
+ {
14266
+ label: "Worst",
14267
+ score: stats.min?.score ?? null,
14268
+ sub: stats.min ? formatDateShort(stats.min.date) : null
14269
+ }
14270
+ )
14271
+ ] })
14272
+ ] }) });
14273
+ }
14274
+ function TrendPill({ stats }) {
14275
+ if (stats.delta === null || stats.count < 2) {
14276
+ return /* @__PURE__ */ jsx69(Badge13, { fontSize: 1, mode: "outline", tone: "default", children: "Single data point" });
14277
+ }
14278
+ const { label, tone } = trendBadgeContent(stats.trend, stats.delta);
14279
+ return /* @__PURE__ */ jsx69(Badge13, { fontSize: 1, mode: "default", tone, children: label });
14280
+ }
14281
+ function trendBadgeContent(trend, delta) {
14282
+ switch (trend) {
14283
+ case "up":
14284
+ return { label: `\u2191 ${formatDelta(delta)} pts vs first`, tone: "positive" };
14285
+ case "down":
14286
+ return { label: `\u2193 ${formatDelta(delta)} pts vs first`, tone: "critical" };
14287
+ case "flat":
14288
+ return { label: "Flat (within \xB11.5)", tone: "default" };
14289
+ }
14290
+ }
14291
+ function StatTile({
14292
+ label,
14293
+ score,
14294
+ sub
14295
+ }) {
14296
+ return /* @__PURE__ */ jsxs52(Stack40, { space: 2, children: [
14297
+ /* @__PURE__ */ jsx69(Text51, { muted: true, size: 1, weight: "medium", children: label }),
14298
+ score === null ? /* @__PURE__ */ jsx69(Text51, { muted: true, size: 3, children: "\u2014" }) : /* @__PURE__ */ jsx69(Badge13, { fontSize: 2, mode: "default", tone: scoreTone(score), children: score.toFixed(1) }),
14299
+ score !== null && sub ? /* @__PURE__ */ jsx69(Text51, { muted: true, size: 1, children: sub }) : null
14300
+ ] });
14301
+ }
14302
+
14303
+ // src/components/timeline/useTimelineData.ts
14304
+ import { useCallback as useCallback40, useEffect as useEffect17, useMemo as useMemo18, useState as useState31 } from "react";
14305
+ import { useClient as useClient13 } from "sanity";
14306
+ function useTimelineData({
14307
+ mode,
14308
+ ownerTeam,
14309
+ rangeDays,
14310
+ source
14311
+ }) {
14312
+ const client = useClient13({ apiVersion: API_VERSION });
14313
+ const [dataPoints, setDataPoints] = useState31([]);
14314
+ const [loading, setLoading] = useState31(true);
14315
+ const [error, setError] = useState31(null);
14316
+ const [reloadCounter, setReloadCounter] = useState31(0);
14317
+ const refresh = useCallback40(() => {
14318
+ setReloadCounter((n) => n + 1);
14319
+ }, []);
14320
+ const effectiveStart = useMemo18(
14321
+ () => effectiveStartDate(rangeDays),
14322
+ [rangeDays]
14323
+ );
14324
+ useEffect17(() => {
14325
+ let cancelled = false;
14326
+ setLoading(true);
14327
+ setError(null);
14328
+ client.fetch(scoreTimelineQuery, {
14329
+ mode,
14330
+ ownerTeam,
14331
+ source,
14332
+ startDate: effectiveStart
14333
+ }).then((results) => {
14334
+ if (cancelled) return;
14335
+ setDataPoints(results ?? []);
14336
+ setLoading(false);
14337
+ }).catch((err) => {
14338
+ if (cancelled) return;
14339
+ setError(
14340
+ err instanceof Error ? err.message : "Failed to load timeline data"
14341
+ );
14342
+ setDataPoints([]);
14343
+ setLoading(false);
14344
+ });
14345
+ return () => {
14346
+ cancelled = true;
14347
+ };
14348
+ }, [client, effectiveStart, mode, ownerTeam, source, reloadCounter]);
14349
+ return {
14350
+ dataPoints,
14351
+ effectiveStart,
14352
+ error,
14353
+ loading,
14354
+ refresh
14355
+ };
14356
+ }
14357
+
14358
+ // src/components/ScoreTimeline.tsx
14359
+ import { Fragment as Fragment16, jsx as jsx70, jsxs as jsxs53 } from "react/jsx-runtime";
14360
+ function buildSearchParams2(filters) {
14361
+ const params = [];
14362
+ if (filters.range !== DEFAULT_RANGE) params.push(["range", filters.range]);
14363
+ if (filters.granularity !== DEFAULT_GRANULARITY) {
14364
+ params.push(["granularity", filters.granularity]);
14365
+ }
14366
+ if (filters.area) params.push(["area", filters.area]);
14367
+ if (filters.mode) params.push(["mode", filters.mode]);
14368
+ if (filters.source) params.push(["source", filters.source]);
14369
+ if (filters.ownerTeam) params.push(["ownerTeam", filters.ownerTeam]);
14370
+ if (filters.ma) params.push(["ma", "1"]);
14371
+ return params;
14372
+ }
14373
+ function ScoreTimeline({
14374
+ mode: defaultMode = null,
14375
+ source: defaultSource = null
14376
+ }) {
14377
+ const router = useRouter4();
14378
+ const routerState = router.state;
14379
+ const urlParams = routerState._searchParams;
14380
+ const [range, setRange] = useState32(() => {
14381
+ const v = readSearchParam(urlParams, "range");
14382
+ return v && isRangePreset(v) ? v : DEFAULT_RANGE;
14383
+ });
14384
+ const [granularity, setGranularity] = useState32(() => {
14385
+ const v = readSearchParam(urlParams, "granularity");
14386
+ return v && isGranularity(v) ? v : DEFAULT_GRANULARITY;
14387
+ });
14388
+ const [area, setArea] = useState32(
14389
+ () => readSearchParam(urlParams, "area")
14390
+ );
14391
+ const [mode, setMode] = useState32(
14392
+ () => readSearchParam(urlParams, "mode") ?? defaultMode
14393
+ );
14394
+ const [source, setSource] = useState32(
14395
+ () => readSearchParam(urlParams, "source") ?? defaultSource
14396
+ );
14397
+ const [ownerTeam, setOwnerTeam] = useState32(
14398
+ () => readSearchParam(urlParams, "ownerTeam")
14399
+ );
14400
+ const [showMovingAverage, setShowMovingAverage] = useState32(
14401
+ () => readSearchParam(urlParams, "ma") === "1"
14402
+ );
14403
+ useSyncSearchParams(
14404
+ () => buildSearchParams2({
14405
+ area,
14406
+ granularity,
14407
+ ma: showMovingAverage,
14408
+ mode,
14409
+ ownerTeam,
14410
+ range,
14411
+ source
14412
+ }),
14413
+ [range, granularity, area, mode, source, ownerTeam, showMovingAverage]
14414
+ );
14415
+ const rangeDays = rangePresetToDays(range);
14416
+ const { dataPoints, error, loading, refresh } = useTimelineData({
14417
+ mode,
14418
+ ownerTeam,
14419
+ rangeDays,
14420
+ source
14421
+ });
14422
+ const areaOptions = useMemo19(
14423
+ () => uniqueSorted(dataPoints, (dp) => dp.scores.map((s) => s.feature)),
14424
+ [dataPoints]
14425
+ );
14426
+ const modeOptions = useMemo19(
14427
+ () => uniqueSorted(dataPoints, (dp) => [dp.mode]),
14428
+ [dataPoints]
14429
+ );
14430
+ const sourceOptions = useMemo19(
14431
+ () => uniqueSorted(dataPoints, (dp) => [dp.source]),
14432
+ [dataPoints]
14433
+ );
14434
+ const ownerTeamOptions = useMemo19(
14435
+ () => uniqueSorted(dataPoints, (dp) => [dp.ownerTeam]),
14436
+ [dataPoints]
14437
+ );
14438
+ const chartPoints = useMemo19(
14439
+ () => aggregate(dataPoints, area, granularity),
14440
+ [area, dataPoints, granularity]
14441
+ );
14442
+ const stats = useMemo19(() => computeStats(chartPoints, 5), [chartPoints]);
14443
+ const handleSelectPoint = useCallback41(
14444
+ (point) => {
14445
+ const id = point.reportId ?? point._id;
14446
+ router.navigate({ reportId: id });
14447
+ },
14448
+ [router]
14449
+ );
14450
+ const handleCopyUrl = useCallback41(() => {
14451
+ if (typeof window === "undefined") return;
14452
+ void copyTextToClipboard(window.location.href);
14453
+ }, []);
14454
+ const handleCopyCsv = useCallback41(() => {
14455
+ void copyTextToClipboard(toCsv(dataPoints, area));
14456
+ }, [area, dataPoints]);
14457
+ const handleExportCsv = useCallback41(() => {
14458
+ const csv = toCsv(dataPoints, area);
14459
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-").slice(0, 19);
14460
+ downloadCsv(csv, `score-timeline-${ts}.csv`);
14461
+ }, [area, dataPoints]);
14462
+ return /* @__PURE__ */ jsxs53(Stack41, { space: 4, children: [
14463
+ /* @__PURE__ */ jsx70(
14464
+ TimelineFilters,
14465
+ {
14466
+ area,
14467
+ areaOptions,
14468
+ granularity,
14469
+ loading,
14470
+ mode,
14471
+ modeOptions,
14472
+ onAreaChange: setArea,
14473
+ onCopyCsv: handleCopyCsv,
14474
+ onCopyUrl: handleCopyUrl,
14475
+ onExportCsv: handleExportCsv,
14476
+ onGranularityChange: setGranularity,
14477
+ onModeChange: setMode,
14478
+ onOwnerTeamChange: setOwnerTeam,
14479
+ onRangeChange: setRange,
14480
+ onRefresh: refresh,
14481
+ onShowMovingAverageChange: setShowMovingAverage,
14482
+ onSourceChange: setSource,
14483
+ ownerTeam,
14484
+ ownerTeamOptions,
14485
+ range,
14486
+ showMovingAverage,
14487
+ source,
14488
+ sourceOptions
14489
+ }
14490
+ ),
14491
+ error ? /* @__PURE__ */ jsx70(Card25, { padding: 4, radius: 2, tone: "critical", children: /* @__PURE__ */ jsxs53(Text52, { size: 2, children: [
14492
+ "Failed to load timeline: ",
14493
+ error
14494
+ ] }) }) : null,
14495
+ loading ? /* @__PURE__ */ jsx70(Card25, { padding: 4, radius: 2, shadow: 1, children: /* @__PURE__ */ jsxs53(Flex38, { align: "center", gap: 2, justify: "center", style: { height: 220 }, children: [
14496
+ /* @__PURE__ */ jsx70(Spinner, { muted: true }),
14497
+ /* @__PURE__ */ jsx70(Text52, { muted: true, size: 2, children: "Loading timeline\u2026" })
14498
+ ] }) }) : /* @__PURE__ */ jsxs53(Fragment16, { children: [
14499
+ /* @__PURE__ */ jsx70(
14500
+ TimelineChart,
14501
+ {
14502
+ movingAverage: stats.movingAverage,
14503
+ onSelectPoint: handleSelectPoint,
14504
+ points: chartPoints,
14505
+ showMovingAverage
14506
+ }
14507
+ ),
14508
+ /* @__PURE__ */ jsx70(TimelineSummary, { area, stats })
13576
14509
  ] })
13577
14510
  ] });
13578
14511
  }
13579
14512
  var ScoreTimeline_default = ScoreTimeline;
13580
14513
 
13581
14514
  // src/components/Dashboard.tsx
13582
- import { jsx as jsx67, jsxs as jsxs51 } from "react/jsx-runtime";
14515
+ import { jsx as jsx71, jsxs as jsxs54 } from "react/jsx-runtime";
13583
14516
  var VIEW_PARAM_MAP = {
13584
14517
  compare: "compare",
13585
14518
  timeline: "timeline"
13586
14519
  };
13587
14520
  function Dashboard() {
13588
- return /* @__PURE__ */ jsx67(HelpProvider, { children: /* @__PURE__ */ jsx67(JudgmentDrawerProvider, { children: /* @__PURE__ */ jsx67(DashboardShell, {}) }) });
14521
+ return /* @__PURE__ */ jsx71(HelpProvider, { children: /* @__PURE__ */ jsx71(JudgmentDrawerProvider, { children: /* @__PURE__ */ jsx71(DashboardShell, {}) }) });
13589
14522
  }
13590
14523
  function DashboardShell() {
13591
- const router = useRouter4();
14524
+ const router = useRouter5();
13592
14525
  const { close: closeDrawer } = useJudgmentDrawer();
13593
14526
  const routerState = router.state;
13594
14527
  const reportId = routerState.reportId ?? null;
13595
- useEffect17(() => {
14528
+ useEffect18(() => {
13596
14529
  if (!reportId) closeDrawer();
13597
14530
  }, [reportId, closeDrawer]);
13598
- const handleJudgmentDrawerClose = useCallback39(() => {
14531
+ const handleJudgmentDrawerClose = useCallback42(() => {
13599
14532
  closeDrawer();
13600
14533
  const state = { ...router.state };
13601
14534
  if (state.focus) {
@@ -13603,21 +14536,21 @@ function DashboardShell() {
13603
14536
  router.navigate(state);
13604
14537
  }
13605
14538
  }, [closeDrawer, router]);
13606
- return /* @__PURE__ */ jsxs51(Flex37, { style: { height: "100%" }, children: [
13607
- /* @__PURE__ */ jsx67(Box34, { flex: 1, overflow: "auto", children: /* @__PURE__ */ jsx67(DashboardContent, {}) }),
13608
- /* @__PURE__ */ jsx67(JudgmentDetailDrawerOutlet, { onClose: handleJudgmentDrawerClose }),
13609
- /* @__PURE__ */ jsx67(HelpDrawer, {})
14539
+ return /* @__PURE__ */ jsxs54(Flex39, { style: { height: "100%" }, children: [
14540
+ /* @__PURE__ */ jsx71(Box37, { flex: 1, overflow: "auto", children: /* @__PURE__ */ jsx71(DashboardContent, {}) }),
14541
+ /* @__PURE__ */ jsx71(JudgmentDetailDrawerOutlet, { onClose: handleJudgmentDrawerClose }),
14542
+ /* @__PURE__ */ jsx71(HelpDrawer, {})
13610
14543
  ] });
13611
14544
  }
13612
14545
  function DashboardContent() {
13613
- const router = useRouter4();
14546
+ const router = useRouter5();
13614
14547
  const { openHelp } = useHelp();
13615
14548
  const routerState = router.state;
13616
14549
  const reportId = routerState.reportId ?? null;
13617
14550
  const isDetail = reportId !== null;
13618
14551
  const activeTab = isDetail ? "latest" : VIEW_PARAM_MAP[routerState.view ?? ""] ?? "latest";
13619
14552
  const defaultTopic = deriveHelpTopic(routerState);
13620
- const navigateToTab = useCallback39(
14553
+ const navigateToTab = useCallback42(
13621
14554
  (tab) => {
13622
14555
  if (tab === "latest") {
13623
14556
  router.navigate({});
@@ -13627,13 +14560,13 @@ function DashboardContent() {
13627
14560
  },
13628
14561
  [router]
13629
14562
  );
13630
- const handleSelectReport = useCallback39(
14563
+ const handleSelectReport = useCallback42(
13631
14564
  (id) => {
13632
14565
  router.navigate({ reportId: id });
13633
14566
  },
13634
14567
  [router]
13635
14568
  );
13636
- const handleTabChange = useCallback39(
14569
+ const handleTabChange = useCallback42(
13637
14570
  (tab, subTab, focus) => {
13638
14571
  if (!routerState.reportId) return;
13639
14572
  const state = {
@@ -13646,20 +14579,20 @@ function DashboardContent() {
13646
14579
  },
13647
14580
  [router, routerState.reportId]
13648
14581
  );
13649
- const handleBack = useCallback39(() => {
14582
+ const handleBack = useCallback42(() => {
13650
14583
  router.navigate({});
13651
14584
  }, [router]);
13652
- const handleOpenHelp = useCallback39(() => {
14585
+ const handleOpenHelp = useCallback42(() => {
13653
14586
  openHelp(defaultTopic);
13654
14587
  }, [openHelp, defaultTopic]);
13655
- return /* @__PURE__ */ jsx67(Container, { width: 4, children: /* @__PURE__ */ jsxs51(Stack40, { padding: 4, space: 4, children: [
13656
- /* @__PURE__ */ jsxs51(Flex37, { align: "center", gap: 3, children: [
13657
- /* @__PURE__ */ jsxs51(Stack40, { flex: 1, space: 1, children: [
13658
- /* @__PURE__ */ jsx67(Text51, { size: 4, weight: "bold", children: "AI Literacy Framework" }),
13659
- /* @__PURE__ */ jsx67(Text51, { muted: true, size: 2, children: "Evaluation reports and score trends" })
14588
+ return /* @__PURE__ */ jsx71(Container, { width: 4, children: /* @__PURE__ */ jsxs54(Stack42, { padding: 4, space: 4, children: [
14589
+ /* @__PURE__ */ jsxs54(Flex39, { align: "center", gap: 3, children: [
14590
+ /* @__PURE__ */ jsxs54(Stack42, { flex: 1, space: 1, children: [
14591
+ /* @__PURE__ */ jsx71(Text53, { size: 4, weight: "bold", children: "AI Literacy Framework" }),
14592
+ /* @__PURE__ */ jsx71(Text53, { muted: true, size: 2, children: "Evaluation reports and score trends" })
13660
14593
  ] }),
13661
- /* @__PURE__ */ jsx67(
13662
- Button13,
14594
+ /* @__PURE__ */ jsx71(
14595
+ Button15,
13663
14596
  {
13664
14597
  icon: HelpCircleIcon8,
13665
14598
  mode: "bleed",
@@ -13669,8 +14602,8 @@ function DashboardContent() {
13669
14602
  }
13670
14603
  )
13671
14604
  ] }),
13672
- !isDetail && /* @__PURE__ */ jsxs51(TabList3, { space: 1, children: [
13673
- /* @__PURE__ */ jsx67(
14605
+ !isDetail && /* @__PURE__ */ jsxs54(TabList3, { space: 1, children: [
14606
+ /* @__PURE__ */ jsx71(
13674
14607
  Tab3,
13675
14608
  {
13676
14609
  "aria-controls": "latest-panel",
@@ -13680,7 +14613,7 @@ function DashboardContent() {
13680
14613
  selected: activeTab === "latest"
13681
14614
  }
13682
14615
  ),
13683
- /* @__PURE__ */ jsx67(
14616
+ /* @__PURE__ */ jsx71(
13684
14617
  Tab3,
13685
14618
  {
13686
14619
  "aria-controls": "timeline-panel",
@@ -13690,7 +14623,7 @@ function DashboardContent() {
13690
14623
  selected: activeTab === "timeline"
13691
14624
  }
13692
14625
  ),
13693
- /* @__PURE__ */ jsx67(
14626
+ /* @__PURE__ */ jsx71(
13694
14627
  Tab3,
13695
14628
  {
13696
14629
  "aria-controls": "compare-panel",
@@ -13701,10 +14634,10 @@ function DashboardContent() {
13701
14634
  }
13702
14635
  )
13703
14636
  ] }),
13704
- !isDetail && activeTab === "latest" && /* @__PURE__ */ jsx67(TabPanel3, { "aria-labelledby": "latest-tab", id: "latest-panel", children: /* @__PURE__ */ jsx67(LatestReports, { onSelectReport: handleSelectReport }) }),
13705
- !isDetail && activeTab === "timeline" && /* @__PURE__ */ jsx67(TabPanel3, { "aria-labelledby": "timeline-tab", id: "timeline-panel", children: /* @__PURE__ */ jsx67(ScoreTimeline_default, {}) }),
13706
- !isDetail && activeTab === "compare" && /* @__PURE__ */ jsx67(TabPanel3, { "aria-labelledby": "compare-tab", id: "compare-panel", children: /* @__PURE__ */ jsx67(ComparisonView, {}) }),
13707
- isDetail && reportId && /* @__PURE__ */ jsx67(
14637
+ !isDetail && activeTab === "latest" && /* @__PURE__ */ jsx71(TabPanel3, { "aria-labelledby": "latest-tab", id: "latest-panel", children: /* @__PURE__ */ jsx71(LatestReports, { onSelectReport: handleSelectReport }) }),
14638
+ !isDetail && activeTab === "timeline" && /* @__PURE__ */ jsx71(TabPanel3, { "aria-labelledby": "timeline-tab", id: "timeline-panel", children: /* @__PURE__ */ jsx71(ScoreTimeline_default, {}) }),
14639
+ !isDetail && activeTab === "compare" && /* @__PURE__ */ jsx71(TabPanel3, { "aria-labelledby": "compare-tab", id: "compare-panel", children: /* @__PURE__ */ jsx71(ComparisonView, {}) }),
14640
+ isDetail && reportId && /* @__PURE__ */ jsx71(
13708
14641
  ReportDetail,
13709
14642
  {
13710
14643
  activeTab: routerState.tab ?? null,
@@ -13772,7 +14705,7 @@ var ailfStructure = (S) => S.list().id("root").title("Content").items([
13772
14705
  // src/actions/RunEvaluationAction.tsx
13773
14706
  import { BarChartIcon as BarChartIcon2 } from "@sanity/icons";
13774
14707
  import { useToast as useToast10 } from "@sanity/ui";
13775
- import { useCallback as useCallback40, useEffect as useEffect18, useRef as useRef9, useState as useState31 } from "react";
14708
+ import { useCallback as useCallback43, useEffect as useEffect19, useRef as useRef10, useState as useState33 } from "react";
13776
14709
  import {
13777
14710
  getReleaseIdFromReleaseDocumentId as getReleaseIdFromReleaseDocumentId3,
13778
14711
  useClient as useClient14,
@@ -13803,10 +14736,10 @@ function createRunEvaluationAction(options = {}) {
13803
14736
  const projectId = useProjectId2();
13804
14737
  const currentUser = useCurrentUser4();
13805
14738
  const toast = useToast10();
13806
- const [state, setState] = useState31({ status: "loading" });
13807
- const requestedAtRef = useRef9(null);
14739
+ const [state, setState] = useState33({ status: "loading" });
14740
+ const requestedAtRef = useRef10(null);
13808
14741
  const perspectiveId = getReleaseIdFromReleaseDocumentId3(release._id);
13809
- useEffect18(() => {
14742
+ useEffect19(() => {
13810
14743
  let cancelled = false;
13811
14744
  client.fetch(contentImpactQuery, buildReportQueryParams(perspectiveId)).then((results) => {
13812
14745
  if (cancelled) return;
@@ -13829,7 +14762,7 @@ function createRunEvaluationAction(options = {}) {
13829
14762
  cancelled = true;
13830
14763
  };
13831
14764
  }, [client, perspectiveId]);
13832
- useEffect18(() => {
14765
+ useEffect19(() => {
13833
14766
  if (state.status !== "requested" && state.status !== "polling") return;
13834
14767
  const { requestId, startedAt } = state;
13835
14768
  if (state.status === "requested") {
@@ -13879,7 +14812,7 @@ function createRunEvaluationAction(options = {}) {
13879
14812
  }, POLL_INTERVAL_MS2);
13880
14813
  return () => clearInterval(interval);
13881
14814
  }, [client, perspectiveId, state]);
13882
- useEffect18(() => {
14815
+ useEffect19(() => {
13883
14816
  if (state.status !== "error") return;
13884
14817
  const timer = setTimeout(() => {
13885
14818
  client.fetch(contentImpactQuery, buildReportQueryParams(perspectiveId)).then((results) => {
@@ -13899,7 +14832,7 @@ function createRunEvaluationAction(options = {}) {
13899
14832
  }, 15e3);
13900
14833
  return () => clearTimeout(timer);
13901
14834
  }, [client, perspectiveId, state]);
13902
- const handleRequest = useCallback40(async () => {
14835
+ const handleRequest = useCallback43(async () => {
13903
14836
  const releaseTitle = release.metadata?.title ?? perspectiveId ?? "release";
13904
14837
  const tag = `release-${slugify3(releaseTitle)}-${dateStamp3()}`;
13905
14838
  const now = Date.now();