sanity-plugin-jev-fields 0.1.0 → 0.2.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.
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ function JevFormProvider({ config, children }) {
39
39
  }
40
40
  function useJevForm() {
41
41
  let context = useContext(Context);
42
- if (!context) throw Error("Jev signals need the jev() plugin in sanity.config.");
42
+ if (!context) throw Error("Jev questions need the jev() plugin in sanity.config.");
43
43
  return context;
44
44
  }
45
45
  /**
@@ -137,12 +137,12 @@ function rangeViolation(value, rule, format) {
137
137
  if (rule?.atLeast !== void 0 && value < rule.atLeast) return `below ${format(rule.atLeast)}`;
138
138
  if (rule?.atMost !== void 0 && value > rule.atMost) return `above ${format(rule.atMost)}`;
139
139
  }
140
- /** Why a choice signal can't be asked, if it can't. */
141
- function choiceProblem(signal, options) {
142
- if (options.length < 2) return "A choice signal needs at least two options.";
143
- if (options.length > 255) return "A choice signal allows at most 255 options.";
140
+ /** Why a choice question can't be asked, if it can't. */
141
+ function choiceProblem(question, options) {
142
+ if (options.length < 2) return "A choice question needs at least two options.";
143
+ if (options.length > 255) return "A choice question allows at most 255 options.";
144
144
  for (let level of ["warn", "require"]) {
145
- let rule = signal[level];
145
+ let rule = question[level];
146
146
  if (!rule) continue;
147
147
  if (!Array.isArray(rule.oneOf) || rule.oneOf.length === 0) return `\`${level}.oneOf\` must list at least one option.`;
148
148
  let unknown = rule.oneOf.find((option) => !options.includes(option));
@@ -150,9 +150,9 @@ function choiceProblem(signal, options) {
150
150
  }
151
151
  }
152
152
  /** One option from a named set. @public */
153
- const choice = (signal) => ({
153
+ const choice = (question) => ({
154
154
  type: "choice",
155
- ...signal
155
+ ...question
156
156
  }), choiceSchemaTypes = [defineType({
157
157
  name: TYPE_NAMES.choiceProbability,
158
158
  title: "Jev choice probability",
@@ -189,14 +189,14 @@ const choice = (signal) => ({
189
189
  ...bookkeepingFields
190
190
  ]
191
191
  })];
192
- function bindChoice(signal) {
193
- let criteria = signal.criteria ?? {}, problem = choiceProblem(signal, Object.keys(criteria));
192
+ function bindChoice(question) {
193
+ let criteria = question.criteria ?? {}, problem = choiceProblem(question, Object.keys(criteria));
194
194
  return {
195
195
  typeName: TYPE_NAMES.choice,
196
- question: problem ? void 0 : {
196
+ gatewayQuestion: problem ? void 0 : {
197
197
  type: "choice",
198
- instructions: signal.instructions,
199
- criteria: signal.criteria
198
+ instructions: question.instructions,
199
+ criteria: question.criteria
200
200
  },
201
201
  problem,
202
202
  toStored(answer) {
@@ -235,17 +235,17 @@ function bindChoice(signal) {
235
235
  },
236
236
  tone: "default",
237
237
  aside: meaning ? { note: meaning } : void 0,
238
- body: <ChoiceBody signal={signal} choice={record.choice} probabilities={probabilities} />
238
+ body: <ChoiceBody question={question} choice={record.choice} probabilities={probabilities} />
239
239
  };
240
240
  },
241
241
  check(level, stored, title) {
242
- let record = storedOf(stored, TYPE_NAMES.choice), rule = signal[level];
242
+ let record = storedOf(stored, TYPE_NAMES.choice), rule = question[level];
243
243
  if (record && typeof record.choice == "string" && rule?.oneOf && !rule.oneOf.includes(record.choice)) return `${title} is "${record.choice}", not ${rule.oneOf.map((option) => `"${option}"`).join(" or ")}.`;
244
244
  }
245
245
  };
246
246
  }
247
- function ChoiceBody({ signal, choice, probabilities }) {
248
- let options = Object.keys(signal.criteria), nameWidth = `${Math.max(...options.map((name) => name.length), 4) + 2}ch`;
247
+ function ChoiceBody({ question, choice, probabilities }) {
248
+ let options = Object.keys(question.criteria), nameWidth = `${Math.max(...options.map((name) => name.length), 4) + 2}ch`;
249
249
  return <Stack gap={3}>
250
250
  {options.map((option) => {
251
251
  let probability = probabilities.find((entry) => entry.option === option)?.probability ?? 0, winner = option === choice, percent = Math.round(probability * 100);
@@ -275,7 +275,7 @@ const JEV_NAMESPACE = "jev", jevLocaleBundle = defineLocaleResourceBundle({
275
275
  resources: {
276
276
  "strip.label": "Jev",
277
277
  "strip.set-up": "Set up Jev",
278
- "strip.evaluate-all": "Evaluate all signals now",
278
+ "strip.evaluate-all": "Evaluate all questions now",
279
279
  "chip.empty": "–",
280
280
  "chip.error": "Error",
281
281
  "chip.out-of-date": "Out of date",
@@ -305,7 +305,7 @@ const JEV_NAMESPACE = "jev", jevLocaleBundle = defineLocaleResourceBundle({
305
305
  "key-dialog.saving": "Saving…",
306
306
  "key-dialog.save-failed": "Could not save the key: {{error}}",
307
307
  "tool.title": "Jev",
308
- "tool.intro": "Jev signals are answered by TypeSafe’s Jev model through Vercel AI Gateway.",
308
+ "tool.intro": "Jev questions are answered by TypeSafe’s Jev model through Vercel AI Gateway.",
309
309
  "tool.key.heading": "AI Gateway API key",
310
310
  "tool.key.status.config": "From plugin config",
311
311
  "tool.key.status.checking": "Checking",
@@ -316,11 +316,11 @@ const JEV_NAMESPACE = "jev", jevLocaleBundle = defineLocaleResourceBundle({
316
316
  "tool.key.ends-with": "Key ending in <Code>{{last4}}</Code>",
317
317
  "tool.key.changed": "Last changed {{date}}",
318
318
  "tool.key.checking": "Checking for a stored key…",
319
- "tool.key.none": "No key is stored, so Jev signals cannot evaluate yet.",
319
+ "tool.key.none": "No key is stored, so Jev questions can’t be evaluated yet.",
320
320
  "tool.key.set": "Set key",
321
321
  "tool.key.change": "Change key",
322
322
  "tool.key.remove": "Remove key",
323
- "tool.key.remove-confirm": "Remove the key? Jev signals stop evaluating until a new one is set.",
323
+ "tool.key.remove-confirm": "Remove the key? Jev questions stop being evaluated until a new one is set.",
324
324
  "tool.key.remove-cancel": "Cancel",
325
325
  "tool.key.remove-confirm-button": "Remove",
326
326
  "tool.key.remove-failed": "Could not remove the key: {{error}}",
@@ -330,9 +330,9 @@ const JEV_NAMESPACE = "jev", jevLocaleBundle = defineLocaleResourceBundle({
330
330
  "tool.visibility.heading": "Who can see the key",
331
331
  "tool.visibility.body": "The key is stored in this dataset in the document <Code>secrets.jev</Code>. It is not public, but anyone who can read the dataset can see it: every Studio user, and every API token with read access, such as a frontend’s read or preview token. It is also included in dataset exports and backups. Use a dedicated key with a spend limit. If editors and tokens must never see the key, set a <Code>transport</Code> that sends requests through your own server."
332
332
  }
333
- }), noul = (signal) => ({
333
+ }), noul = (question) => ({
334
334
  type: "noul",
335
- ...signal
335
+ ...question
336
336
  }), noulSchemaTypes = [defineType({
337
337
  name: TYPE_NAMES.noul,
338
338
  title: "Jev yes/no",
@@ -348,16 +348,16 @@ const JEV_NAMESPACE = "jev", jevLocaleBundle = defineLocaleResourceBundle({
348
348
  caution: "medium",
349
349
  positive: "high"
350
350
  };
351
- function bindNoul(signal) {
352
- let problem = !signal.true?.trim() || !signal.false?.trim() ? "A noul signal needs both `true` and `false` text." : rangeRuleProblem(signal, 0, 1);
351
+ function bindNoul(question) {
352
+ let problem = !question.true?.trim() || !question.false?.trim() ? "A noul question needs both `true` and `false` text." : rangeRuleProblem(question, 0, 1);
353
353
  return {
354
354
  typeName: TYPE_NAMES.noul,
355
- question: problem ? void 0 : {
355
+ gatewayQuestion: problem ? void 0 : {
356
356
  type: "boolean",
357
- instructions: signal.instructions,
357
+ instructions: question.instructions,
358
358
  criteria: {
359
- true: signal.true,
360
- false: signal.false
359
+ true: question.true,
360
+ false: question.false
361
361
  }
362
362
  },
363
363
  problem,
@@ -386,38 +386,38 @@ function bindNoul(signal) {
386
386
  level: LEVELS$1[tone] ?? "medium",
387
387
  tone
388
388
  },
389
- body: <NoulBody signal={signal} probability={probability} />
389
+ body: <NoulBody question={question} probability={probability} />
390
390
  };
391
391
  },
392
392
  check(level, stored, title) {
393
393
  let record = storedOf(stored, TYPE_NAMES.noul);
394
394
  if (!record || typeof record.probability != "number") return;
395
- let percent = (value) => `${Math.round(value * 100)}%`, broken = rangeViolation(record.probability, signal[level], percent);
395
+ let percent = (value) => `${Math.round(value * 100)}%`, broken = rangeViolation(record.probability, question[level], percent);
396
396
  return broken && `${title} is ${percent(record.probability)}, ${broken}.`;
397
397
  }
398
398
  };
399
399
  }
400
- function NoulBody({ signal, probability }) {
400
+ function NoulBody({ question, probability }) {
401
401
  let { t } = useTranslation("jev"), percent = Math.round(probability * 100);
402
402
  return <Stack gap={4}>
403
403
  <Flex align="baseline" gap={2}>
404
404
  <Text size={4} weight="medium">
405
405
  {percent}%
406
406
  </Text>
407
- {signal.label && <Text size={1} muted>
408
- {signal.label}
407
+ {question.label && <Text size={1} muted>
408
+ {question.label}
409
409
  </Text>}
410
410
  </Flex>
411
- <Bar fraction={probability} color={trafficColor(probability)} label={`${signal.title ?? t("noul.yes")}: ${percent}%`} />
411
+ <Bar fraction={probability} color={trafficColor(probability)} label={`${question.title ?? t("noul.yes")}: ${percent}%`} />
412
412
  <Text size={1} muted>
413
- {probability >= .5 ? signal.true : signal.false}
413
+ {probability >= .5 ? question.true : question.false}
414
414
  </Text>
415
415
  </Stack>;
416
416
  }
417
417
  /** A position on an ordered scale. @public */
418
- const score = (signal) => ({
418
+ const score = (question) => ({
419
419
  type: "score",
420
- ...signal
420
+ ...question
421
421
  }), scoreSchemaTypes = [defineType({
422
422
  name: TYPE_NAMES.score,
423
423
  title: "Jev score",
@@ -461,24 +461,24 @@ function segmentFills(score, count) {
461
461
  return Array.from({ length: count }, (_, index) => Math.min(1, Math.max(0, score - index + 1)));
462
462
  }
463
463
  /** Colour and tone of a score, from the criterion it is nearest to. */
464
- function scoreLook(signal, value, max) {
465
- if (signal.colors === "neutral") return {
464
+ function scoreLook(question, value, max) {
465
+ if (question.colors === "neutral") return {
466
466
  color: NEUTRAL_COLOR,
467
467
  tone: "primary"
468
468
  };
469
- let fraction = max > 0 ? Math.round(value) / max : 0, good = signal.colors === "reverse" ? 1 - fraction : fraction;
469
+ let fraction = max > 0 ? Math.round(value) / max : 0, good = question.colors === "reverse" ? 1 - fraction : fraction;
470
470
  return {
471
471
  color: trafficColor(good),
472
472
  tone: trafficTone(good)
473
473
  };
474
474
  }
475
- function bindScore(signal) {
476
- let { criteria } = signal, problem = !Array.isArray(criteria) || criteria.length < 2 ? "A score signal needs at least two criteria." : criteria.length > 10 ? "A score signal allows at most ten criteria." : rangeRuleProblem(signal, 0, criteria.length - 1);
475
+ function bindScore(question) {
476
+ let { criteria } = question, problem = !Array.isArray(criteria) || criteria.length < 2 ? "A score question needs at least two criteria." : criteria.length > 10 ? "A score question allows at most ten criteria." : rangeRuleProblem(question, 0, criteria.length - 1);
477
477
  return {
478
478
  typeName: TYPE_NAMES.score,
479
- question: problem ? void 0 : {
479
+ gatewayQuestion: problem ? void 0 : {
480
480
  type: "score",
481
- instructions: signal.instructions,
481
+ instructions: question.instructions,
482
482
  criteria
483
483
  },
484
484
  problem,
@@ -496,7 +496,7 @@ function bindScore(signal) {
496
496
  read(stored) {
497
497
  let record = storedOf(stored, TYPE_NAMES.score);
498
498
  if (!record || typeof record.score != "number" || typeof record.max != "number") return;
499
- let { score: value, max } = record, label = text(record.label), look = scoreLook(signal, value, max);
499
+ let { score: value, max } = record, label = text(record.label), look = scoreLook(question, value, max);
500
500
  return {
501
501
  value: {
502
502
  ...bookkeepingOf(record),
@@ -515,7 +515,7 @@ function bindScore(signal) {
515
515
  badge: capitalize(label ?? shortLabel(criteria?.[Math.round(value)])),
516
516
  tone: look.tone
517
517
  },
518
- body: <ScoreBody signal={signal} score={value} max={max} color={look.color} />
518
+ body: <ScoreBody question={question} score={value} max={max} color={look.color} />
519
519
  };
520
520
  },
521
521
  check(level, stored, title) {
@@ -524,16 +524,16 @@ function bindScore(signal) {
524
524
  let describe = (position) => {
525
525
  let label = shortLabel(criteria[Math.round(position)]), number = Number.isInteger(position) ? String(position) : position.toFixed(1);
526
526
  return label ? `${number} (${label})` : number;
527
- }, broken = rangeViolation(record.score, signal[level], describe);
527
+ }, broken = rangeViolation(record.score, question[level], describe);
528
528
  return broken && `${title} is ${describe(record.score)}, ${broken}.`;
529
529
  }
530
530
  };
531
531
  }
532
- function ScoreBody({ signal, score, max, color }) {
533
- let { t } = useTranslation("jev"), nearest = Math.round(score), fills = segmentFills(score, signal.criteria.length), next = signal.criteria[nearest + 1];
532
+ function ScoreBody({ question, score, max, color }) {
533
+ let { t } = useTranslation("jev"), nearest = Math.round(score), fills = segmentFills(score, question.criteria.length), next = question.criteria[nearest + 1];
534
534
  return <Stack gap={4}>
535
535
  <Flex gap={1}>
536
- {signal.criteria.map((criterion, index) => <Stack key={index} gap={2} flex={1}>
536
+ {question.criteria.map((criterion, index) => <Stack key={index} gap={2} flex={1}>
537
537
  <Bar fraction={fills[index]} color={color} label={`${index}: ${shortLabel(criterion)}`} />
538
538
  <Text size={1} weight={index === nearest ? "medium" : "regular"} muted={index > nearest} style={index < nearest ? { color } : void 0} textOverflow="ellipsis">
539
539
  {index} · {capitalize(shortLabel(criterion))}
@@ -542,7 +542,7 @@ function ScoreBody({ signal, score, max, color }) {
542
542
  </Flex>
543
543
  <Text size={1} muted>
544
544
  {t("score.summary", {
545
- meaning: capitalize(meaningOf(signal.criteria[nearest])),
545
+ meaning: capitalize(meaningOf(question.criteria[nearest])),
546
546
  score: score.toFixed(1),
547
547
  max
548
548
  })}
@@ -556,23 +556,23 @@ const KIND_SCHEMA_TYPES = [
556
556
  ...scoreSchemaTypes,
557
557
  ...choiceSchemaTypes
558
558
  ];
559
- /** Binds a signal to its kind. The one place that tells Noul, Score and Choice apart. */
560
- function kindOf(signal) {
561
- let kind = bind(signal);
562
- return signal.instructions?.trim() ? kind : {
559
+ /** Binds a question to its kind. The one place that tells Noul, Score and Choice apart. */
560
+ function kindOf(question) {
561
+ let kind = bind(question);
562
+ return question.instructions?.trim() ? kind : {
563
563
  ...kind,
564
- question: void 0,
565
- problem: "Add instructions to this signal."
564
+ gatewayQuestion: void 0,
565
+ problem: "Add instructions to this question."
566
566
  };
567
567
  }
568
- function bind(signal) {
569
- switch (signal.type) {
570
- case "noul": return bindNoul(signal);
571
- case "score": return bindScore(signal);
572
- case "choice": return bindChoice(signal);
568
+ function bind(question) {
569
+ switch (question.type) {
570
+ case "noul": return bindNoul(question);
571
+ case "score": return bindScore(question);
572
+ case "choice": return bindChoice(question);
573
573
  default: return {
574
574
  typeName: "unknown",
575
- problem: "Unknown signal type.",
575
+ problem: "Unknown question type.",
576
576
  toStored: mismatch,
577
577
  read: () => void 0,
578
578
  check: () => void 0
@@ -580,19 +580,19 @@ function bind(signal) {
580
580
  }
581
581
  }
582
582
  /** `readable` → `Readable`, `readingLevel` → `Reading level`. */
583
- function titleOf(key, signal) {
584
- if (signal.title) return signal.title;
583
+ function titleOf(key, question) {
584
+ if (question.title) return question.title;
585
585
  let words = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ");
586
586
  return words.charAt(0).toUpperCase() + words.slice(1).toLowerCase();
587
587
  }
588
588
  const isRecord = (value) => !!value && typeof value == "object" && !Array.isArray(value);
589
- function signalsOf(definition) {
589
+ function questionsOf(definition) {
590
590
  if (!isRecord(definition) || !isRecord(definition.options)) return;
591
591
  let { jev } = definition.options;
592
592
  return isRecord(jev) ? jev : void 0;
593
593
  }
594
594
  /**
595
- * Adds a field that stores each signal's answer next to every field with `options.jev`.
595
+ * Adds a field that stores each question's answer next to every field with `options.jev`.
596
596
  * Plugins can't see the Studio's own schema types, so wrap them in `sanity.config`:
597
597
  *
598
598
  * ```ts
@@ -612,8 +612,8 @@ function withAnswerFields(definition) {
612
612
  of: definition.of.map(withAnswerFields)
613
613
  }), Array.isArray(definition.fields)) {
614
614
  let existing = new Map(definition.fields.filter(isRecord).map((field) => [field.name, field.type])), fields = definition.fields.flatMap((field) => {
615
- let rewritten = withAnswerFields(field), signals = signalsOf(field);
616
- return !signals || !isRecord(field) || !isRecord(rewritten) ? [rewritten] : [withRules(rewritten, signals), ...answerFieldsFor(field, signals, existing)];
615
+ let rewritten = withAnswerFields(field), questions = questionsOf(field);
616
+ return !questions || !isRecord(field) || !isRecord(rewritten) ? [rewritten] : [withRules(rewritten, questions), ...answerFieldsFor(field, questions, existing)];
617
617
  });
618
618
  result = {
619
619
  ...result,
@@ -622,14 +622,14 @@ function withAnswerFields(definition) {
622
622
  }
623
623
  return result;
624
624
  }
625
- function answerFieldsFor(field, signals, existing) {
626
- return Object.entries(signals).flatMap(([key, signal]) => {
627
- let type = TYPE_NAMES[signal.type];
625
+ function answerFieldsFor(field, questions, existing) {
626
+ return Object.entries(questions).flatMap(([key, question]) => {
627
+ let type = TYPE_NAMES[question.type];
628
628
  if (existing.get(key) === type) return [];
629
- if (existing.has(key)) throw Error(`Jev: the signal "${key}" on "${String(field.name)}" needs a field called "${key}" to store its answer, but that name is already taken. Rename the signal.`);
629
+ if (existing.has(key)) throw Error(`Jev: the question "${key}" on "${String(field.name)}" needs a field called "${key}" to store its answer, but that name is already taken. Rename the question.`);
630
630
  return existing.set(key, type), [defineField({
631
631
  name: key,
632
- title: titleOf(key, signal),
632
+ title: titleOf(key, question),
633
633
  type,
634
634
  group: typeof field.group == "string" || Array.isArray(field.group) ? field.group : void 0,
635
635
  fieldset: typeof field.fieldset == "string" ? field.fieldset : void 0
@@ -638,14 +638,14 @@ function answerFieldsFor(field, signals, existing) {
638
638
  }
639
639
  const LEVELS = ["warn", "require"];
640
640
  /**
641
- * Adds a validation rule to the attached field for each signal's `warn` and `require`, after
641
+ * Adds a validation rule to the attached field for each question's `warn` and `require`, after
642
642
  * the field's own validation. Rules judge the stored answer, so they say nothing until there is
643
643
  * one.
644
644
  */
645
- function withRules(field, signals) {
646
- let checks = Object.entries(signals).flatMap(([key, signal]) => LEVELS.filter((level) => signal[level]).map((level) => ({
645
+ function withRules(field, questions) {
646
+ let checks = Object.entries(questions).flatMap(([key, question]) => LEVELS.filter((level) => question[level]).map((level) => ({
647
647
  key,
648
- signal,
648
+ question,
649
649
  level
650
650
  })));
651
651
  if (checks.length === 0) return field;
@@ -653,8 +653,8 @@ function withRules(field, signals) {
653
653
  let ownRules = (Array.isArray(own) ? own : own ? [own] : []).flatMap((entry) => {
654
654
  let result = typeof entry == "function" ? entry(rule) : entry;
655
655
  return Array.isArray(result) ? result : result ? [result] : [];
656
- }), jevRules = checks.map(({ key, signal, level }) => {
657
- let kind = kindOf(signal), title = titleOf(key, signal), custom = rule.custom((_value, context) => {
656
+ }), jevRules = checks.map(({ key, question, level }) => {
657
+ let kind = kindOf(question), title = titleOf(key, question), custom = rule.custom((_value, context) => {
658
658
  let stored = isRecord(context.parent) ? context.parent[key] : void 0;
659
659
  return kind.check(level, stored, title) ?? !0;
660
660
  });
@@ -768,12 +768,12 @@ const pulse = keyframes`
768
768
  background: ${({ $color }) => $color};
769
769
  animation: ${({ $pulse }) => $pulse ? pulse : "none"} 1s ease-in-out infinite;
770
770
  `;
771
- function Chip({ signal, selected, onSelect }) {
772
- let { t } = useTranslation("jev"), { text, color } = signal.reading?.chip ?? {
771
+ function Chip({ question, selected, onSelect }) {
772
+ let { t } = useTranslation("jev"), { text, color } = question.reading?.chip ?? {
773
773
  text: t("chip.empty"),
774
774
  color: void 0
775
- }, failed = !!signal.error || !!signal.problem;
776
- return <Card as="button" type="button" radius={2} paddingX={2} paddingY={2} onClick={onSelect} aria-pressed={selected} title={signal.stale ? t("chip.out-of-date") : void 0} style={{
775
+ }, failed = !!question.error || !!question.problem;
776
+ return <Card as="button" type="button" radius={2} paddingX={2} paddingY={2} onClick={onSelect} aria-pressed={selected} title={question.stale ? t("chip.out-of-date") : void 0} style={{
777
777
  display: "inline-flex",
778
778
  flex: "none",
779
779
  width: "auto",
@@ -782,15 +782,15 @@ function Chip({ signal, selected, onSelect }) {
782
782
  background: selected ? "rgba(128, 128, 128, 0.16)" : void 0
783
783
  }}>
784
784
  <Flex align="center" gap={2}>
785
- <Dot aria-hidden $color={failed ? "#f03e2f" : color ?? "hsl(215 12% 55%)"} $pulse={signal.loading} />
785
+ <Dot aria-hidden $color={failed ? "#f03e2f" : color ?? "hsl(215 12% 55%)"} $pulse={question.loading} />
786
786
  {}
787
787
  <Text size={1}>
788
- {signal.title}
788
+ {question.title}
789
789
  <span style={{
790
790
  marginLeft: "0.6em",
791
791
  fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
792
792
  color: failed ? "#f03e2f" : color,
793
- opacity: signal.stale ? .5 : 1
793
+ opacity: question.stale ? .5 : 1
794
794
  }}>
795
795
  {failed ? t("chip.error") : text}
796
796
  </span>
@@ -798,8 +798,8 @@ function Chip({ signal, selected, onSelect }) {
798
798
  </Flex>
799
799
  </Card>;
800
800
  }
801
- /** The row of signal chips under an attached field. */
802
- function SignalStrip({ signals, selected, onSelect, setup, canRun, loading, onRunAll, onSetUp }) {
801
+ /** The row of question chips under an attached field. */
802
+ function QuestionStrip({ questions, selected, onSelect, setup, canRun, loading, onRunAll, onSetUp }) {
803
803
  let { t } = useTranslation("jev");
804
804
  return <Card border padding={1} style={{
805
805
  borderTop: 0,
@@ -809,7 +809,7 @@ function SignalStrip({ signals, selected, onSelect, setup, canRun, loading, onRu
809
809
  }}>
810
810
  <Flex align="center" gap={2}>
811
811
  <Flex flex={1} align="center" gap={1} wrap="wrap">
812
- {signals.map((signal) => <Chip key={signal.key} signal={signal} selected={selected === signal.key} onSelect={() => onSelect(selected === signal.key ? null : signal.key)} />)}
812
+ {questions.map((question) => <Chip key={question.key} question={question} selected={selected === question.key} onSelect={() => onSelect(selected === question.key ? null : question.key)} />)}
813
813
  </Flex>
814
814
  {setup === "missing" ? <Button text={t("strip.set-up")} mode="ghost" tone="primary" fontSize={1} padding={2} onClick={onSetUp} /> : <Flex align="center" gap={1}>
815
815
  <Text size={0} muted>
@@ -823,32 +823,32 @@ function SignalStrip({ signals, selected, onSelect, setup, canRun, loading, onRu
823
823
  </Card>;
824
824
  }
825
825
  /** Expanded view of the selected chip. */
826
- function SignalDetail({ signal, empty, onRetry, onUpdateKey }) {
827
- let { t } = useTranslation("jev"), { reading } = signal, body = signal.problem ? <Text size={1}>{signal.problem}</Text> : reading ? reading.body : <Text size={1} muted>
826
+ function QuestionDetail({ question, empty, onRetry, onUpdateKey }) {
827
+ let { t } = useTranslation("jev"), { reading } = question, body = question.problem ? <Text size={1}>{question.problem}</Text> : reading ? reading.body : <Text size={1} muted>
828
828
  {t(empty ? "detail.empty-field" : "detail.not-evaluated")}
829
829
  </Text>, aside = reading?.aside ? "badge" in reading.aside ? <Badge tone={reading.aside.tone}>{reading.aside.badge}</Badge> : "level" in reading.aside ? <Badge tone={reading.aside.tone}>{t(`noul.level.${reading.aside.level}`)}</Badge> : <Text size={1} muted>
830
830
  {reading.aside.note}
831
831
  </Text> : null;
832
- return <Card padding={4} radius={2} border tone={signal.problem ? "critical" : reading?.tone ?? "default"}>
832
+ return <Card padding={4} radius={2} border tone={question.problem ? "critical" : reading?.tone ?? "default"}>
833
833
  <Stack gap={4}>
834
834
  <Flex align="center" gap={3}>
835
835
  <Box flex={1}>
836
836
  <Text size={1} weight="semibold">
837
- {signal.title}
837
+ {question.title}
838
838
  </Text>
839
839
  </Box>
840
840
  {aside}
841
841
  </Flex>
842
842
  {body}
843
- {signal.stale && !signal.loading && <Text size={0} muted>
843
+ {question.stale && !question.loading && <Text size={0} muted>
844
844
  {t("detail.out-of-date")}
845
845
  </Text>}
846
- {signal.error && <Card padding={3} radius={2} tone="critical" border>
846
+ {question.error && <Card padding={3} radius={2} tone="critical" border>
847
847
  <Flex align="center" gap={3} wrap="wrap">
848
848
  <Box flex={1}>
849
- <Text size={1}>{errorText(t, signal.error)}</Text>
849
+ <Text size={1}>{errorText(t, question.error)}</Text>
850
850
  </Box>
851
- {signal.keyRejected && onUpdateKey ? <Button text={t("detail.update-key")} mode="ghost" fontSize={1} onClick={onUpdateKey} /> : <Button text={t("detail.try-again")} mode="ghost" fontSize={1} onClick={onRetry} />}
851
+ {question.keyRejected && onUpdateKey ? <Button text={t("detail.update-key")} mode="ghost" fontSize={1} onClick={onUpdateKey} /> : <Button text={t("detail.try-again")} mode="ghost" fontSize={1} onClick={onRetry} />}
852
852
  </Flex>
853
853
  </Card>}
854
854
  </Stack>
@@ -972,13 +972,13 @@ const realClock = {
972
972
  return () => clearTimeout(timeout);
973
973
  }
974
974
  }, EMPTY = {
975
- signals: [],
975
+ questions: [],
976
976
  canRun: !1,
977
977
  empty: !0,
978
978
  loading: !1
979
979
  };
980
980
  /**
981
- * The Evaluation lifecycle of the signals on one attached field. Signals evaluate on their own
981
+ * The Evaluation lifecycle of the questions on one attached field. Questions evaluate on their own
982
982
  * shortly after a local edit, only while the field's state is one this Studio produced; work for
983
983
  * a fingerprint that has since changed is cancelled and its answer dropped.
984
984
  */
@@ -986,16 +986,16 @@ function createLifecycle(clock = realClock) {
986
986
  let inputs, entries = [], localState, expectLocal = !1, requests = /* @__PURE__ */ new Map(), statuses = /* @__PURE__ */ new Map(), attempted = /* @__PURE__ */ new Map(), scheduled, snapshot = EMPTY, dirty = !0, listeners = /* @__PURE__ */ new Set(), canRun = (current) => !!current.transport && !current.readOnly && current.state.trim() !== "";
987
987
  function update(next) {
988
988
  let previous = inputs;
989
- inputs = next, next.state !== previous?.state && (localState = expectLocal ? next.state : void 0, expectLocal = !1), (!previous || next.signals !== previous.signals || next.state !== previous.state || next.model !== previous.model) && (entries = Object.entries(next.signals).map(([key, signal]) => {
990
- let kind = kindOf(signal);
989
+ inputs = next, next.state !== previous?.state && (localState = expectLocal ? next.state : void 0, expectLocal = !1), (!previous || next.questions !== previous.questions || next.state !== previous.state || next.model !== previous.model) && (entries = Object.entries(next.questions).map(([key, question]) => {
990
+ let kind = kindOf(question);
991
991
  return {
992
992
  key,
993
- signal,
993
+ question,
994
994
  kind,
995
- title: titleOf(key, signal),
995
+ title: titleOf(key, question),
996
996
  hash: fingerprint([
997
997
  next.state,
998
- kind.question ?? null,
998
+ kind.gatewayQuestion ?? null,
999
999
  next.model
1000
1000
  ])
1001
1001
  };
@@ -1007,11 +1007,11 @@ function createLifecycle(clock = realClock) {
1007
1007
  for (let [key, status] of statuses) hashes.get(key) !== status.hash && statuses.delete(key);
1008
1008
  for (let [key, hash] of attempted) hashes.get(key) !== hash && attempted.delete(key);
1009
1009
  }
1010
- /** Signals that would evaluate on their own: unanswered or stale, and not tried yet. */
1010
+ /** Questions that would evaluate on their own: unanswered or stale, and not tried yet. */
1011
1011
  function pendingKeys(current, views) {
1012
1012
  return !canRun(current) || localState !== current.state ? [] : entries.filter((entry, index) => {
1013
1013
  let view = views[index];
1014
- return entry.kind.question && (!view.reading || view.stale) && attempted.get(entry.key) !== entry.hash;
1014
+ return entry.kind.gatewayQuestion && (!view.reading || view.stale) && attempted.get(entry.key) !== entry.hash;
1015
1015
  }).map(({ key }) => key);
1016
1016
  }
1017
1017
  /** Rebuilds the snapshot and the debounce after anything they depend on changed. */
@@ -1023,7 +1023,7 @@ function createLifecycle(clock = realClock) {
1023
1023
  let reading = entry.kind.read(current.answers[entry.key]), status = statuses.get(entry.key);
1024
1024
  return {
1025
1025
  key: entry.key,
1026
- signal: entry.signal,
1026
+ question: entry.question,
1027
1027
  title: entry.title,
1028
1028
  problem: entry.kind.problem,
1029
1029
  reading,
@@ -1035,7 +1035,7 @@ function createLifecycle(clock = realClock) {
1035
1035
  }), pending = pendingKeys(current, views);
1036
1036
  for (let view of views) pending.includes(view.key) && (view.loading = !0);
1037
1037
  snapshot = {
1038
- signals: views,
1038
+ questions: views,
1039
1039
  canRun: canRun(current),
1040
1040
  empty: current.state.trim() === "",
1041
1041
  loading: views.some((view) => view.loading)
@@ -1057,8 +1057,8 @@ function createLifecycle(clock = realClock) {
1057
1057
  if (!current?.transport || !canRun(current)) return;
1058
1058
  let { transport, model, state } = current;
1059
1059
  for (let entry of entries) {
1060
- let question = entry.kind.question;
1061
- if (keys && !keys.includes(entry.key) || !question) continue;
1060
+ let gatewayQuestion = entry.kind.gatewayQuestion;
1061
+ if (keys && !keys.includes(entry.key) || !gatewayQuestion) continue;
1062
1062
  requests.get(entry.key)?.controller.abort();
1063
1063
  let controller = new AbortController();
1064
1064
  requests.set(entry.key, {
@@ -1074,7 +1074,7 @@ function createLifecycle(clock = realClock) {
1074
1074
  transport,
1075
1075
  model,
1076
1076
  state,
1077
- question,
1077
+ question: gatewayQuestion,
1078
1078
  tags: [...current.tags, tag],
1079
1079
  signal: controller.signal
1080
1080
  });
@@ -1123,7 +1123,7 @@ function createLifecycle(clock = realClock) {
1123
1123
  }
1124
1124
  const NO_ANSWERS = {};
1125
1125
  /** Connects the Evaluation lifecycle of one attached field to the Studio's form. */
1126
- function useSignals({ signals, path, state, readOnly }) {
1126
+ function useQuestions({ questions, path, state, readOnly }) {
1127
1127
  let { config, writers } = useJevForm(), parentPath = useMemo(() => path.slice(0, -1), [path]), keySource = useKeySource(config), apiKey = keySource.apiKey, transport = useMemo(() => config.transport ?? (apiKey ? gatewayTransport(apiKey, config.endpoint) : void 0), [
1128
1128
  config.transport,
1129
1129
  apiKey,
@@ -1135,7 +1135,7 @@ function useSignals({ signals, path, state, readOnly }) {
1135
1135
  }, [writers, parentPath]), tagPath = useMemo(() => [typeof documentType == "string" ? documentType : "", ...parentPath.map((segment) => typeof segment == "string" ? segment : "[]")], [documentType, parentPath]), [lifecycle] = useState(() => createLifecycle());
1136
1136
  return useLayoutEffect(() => {
1137
1137
  lifecycle.update({
1138
- signals,
1138
+ questions,
1139
1139
  state,
1140
1140
  answers: isRecord$1(parent) ? parent : NO_ANSWERS,
1141
1141
  transport,
@@ -1156,10 +1156,10 @@ function useSignals({ signals, path, state, readOnly }) {
1156
1156
  localEdit: lifecycle.localEdit
1157
1157
  };
1158
1158
  }
1159
- /** Renders a field with `options.jev` as usual, with its signal strip and details below. */
1160
- function AttachedInput({ signals, ...props }) {
1161
- let { t } = useTranslation("jev"), parentOnChange = props.onChange, { value } = props, state = toText(value), jev = useSignals({
1162
- signals,
1159
+ /** Renders a field with `options.jev` as usual, with its question strip and details below. */
1160
+ function AttachedInput({ questions, ...props }) {
1161
+ let { t } = useTranslation("jev"), parentOnChange = props.onChange, { value } = props, state = toText(value), jev = useQuestions({
1162
+ questions,
1163
1163
  path: props.path,
1164
1164
  state,
1165
1165
  readOnly: !!props.readOnly
@@ -1168,11 +1168,11 @@ function AttachedInput({ signals, ...props }) {
1168
1168
  }, inputProps = {
1169
1169
  ...props,
1170
1170
  onChange
1171
- }, [selected, setSelected] = useState(null), [keyDialogOpen, setKeyDialogOpen] = useState(!1), selectedSignal = jev.signals.find((signal) => signal.key === selected);
1171
+ }, [selected, setSelected] = useState(null), [keyDialogOpen, setKeyDialogOpen] = useState(!1), selectedQuestion = jev.questions.find((question) => question.key === selected);
1172
1172
  return <Stack gap={2}>
1173
1173
  <div>
1174
1174
  {props.renderDefault(inputProps)}
1175
- <SignalStrip signals={jev.signals} selected={selected} onSelect={setSelected} setup={jev.setup} canRun={jev.canRun} loading={jev.loading} onRunAll={jev.runAll} onSetUp={() => setKeyDialogOpen(!0)} />
1175
+ <QuestionStrip questions={jev.questions} selected={selected} onSelect={setSelected} setup={jev.setup} canRun={jev.canRun} loading={jev.loading} onRunAll={jev.runAll} onSetUp={() => setKeyDialogOpen(!0)} />
1176
1176
  </div>
1177
1177
  {nearTokenLimit(state) && <Card padding={3} radius={2} tone="caution" border>
1178
1178
  <Text size={1}>
@@ -1182,7 +1182,7 @@ function AttachedInput({ signals, ...props }) {
1182
1182
  })}
1183
1183
  </Text>
1184
1184
  </Card>}
1185
- {selectedSignal && <SignalDetail signal={selectedSignal} empty={jev.empty} onRetry={() => jev.run(selectedSignal.key)} onUpdateKey={jev.keyInSecrets ? () => setKeyDialogOpen(!0) : void 0} />}
1185
+ {selectedQuestion && <QuestionDetail question={selectedQuestion} empty={jev.empty} onRetry={() => jev.run(selectedQuestion.key)} onUpdateKey={jev.keyInSecrets ? () => setKeyDialogOpen(!0) : void 0} />}
1186
1186
  {keyDialogOpen && <JevKeyDialog onClose={() => setKeyDialogOpen(!1)} />}
1187
1187
  </Stack>;
1188
1188
  }
@@ -1190,7 +1190,7 @@ const Code = ({ children }) => <code>{children}</code>, CONNECTION_TEST = kindOf
1190
1190
  instructions: "Is this text a connection test?",
1191
1191
  true: "It is a test",
1192
1192
  false: "It is not a test"
1193
- })).question, asSignalError = (error) => error instanceof JevError ? {
1193
+ })).gatewayQuestion, asQuestionError = (error) => error instanceof JevError ? {
1194
1194
  message: error.message,
1195
1195
  kind: error.kind,
1196
1196
  status: error.status,
@@ -1217,7 +1217,7 @@ function JevTool({ config }) {
1217
1217
  } catch (error) {
1218
1218
  setTest({
1219
1219
  state: "failed",
1220
- error: asSignalError(error)
1220
+ error: asQuestionError(error)
1221
1221
  });
1222
1222
  }
1223
1223
  }
@@ -1311,7 +1311,7 @@ function KeyStatus({ fromConfig, loading, stored }) {
1311
1311
  return fromConfig ? <Badge tone="primary">{t("tool.key.status.config")}</Badge> : loading ? <Badge>{t("tool.key.status.checking")}</Badge> : stored ? <Badge tone="positive">{t("tool.key.status.set")}</Badge> : <Badge tone="caution">{t("tool.key.status.not-set")}</Badge>;
1312
1312
  }
1313
1313
  /**
1314
- * Jev signals for Sanity Studio: yes/no, score and choice questions attached to a field with
1314
+ * Jev questions for Sanity Studio: yes/no, score and choice questions attached to a field with
1315
1315
  * `options.jev`, answered by TypeSafe's Jev model through Vercel AI Gateway. Pair it with
1316
1316
  * `withJevAnswers(schemaTypes)` in `schema.types`.
1317
1317
  * @public
@@ -1328,8 +1328,8 @@ const jev = definePlugin((config) => {
1328
1328
  }],
1329
1329
  form: { components: { input: (props) => {
1330
1330
  if (props.path.length === 0) return <JevFormProvider config={pluginConfig}>{props.renderDefault(props)}</JevFormProvider>;
1331
- let signals = signalsOf(props.schemaType);
1332
- return signals ? <AttachedInput {...props} signals={signals} /> : props.renderDefault(props);
1331
+ let questions = questionsOf(props.schemaType);
1332
+ return questions ? <AttachedInput {...props} questions={questions} /> : props.renderDefault(props);
1333
1333
  } } },
1334
1334
  schema: { types: KIND_SCHEMA_TYPES },
1335
1335
  i18n: { bundles: [jevLocaleBundle] }