@dudousxd/nestjs-agent-react 0.11.0 → 0.12.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/README.md CHANGED
@@ -186,12 +186,32 @@ const transcript = useChatTranscript({
186
186
  });
187
187
 
188
188
  // block.kind === 'elicitation' →
189
- // { toolCallId, preamble, questionCount, isPending, outcome, error,
190
- // questions: [{ id, prompt, position, multiple, selected, isPristine,
189
+ // { toolCallId, preamble, questionCount, isPending, isValid, outcome, error,
190
+ // questions: [{ id, prompt, description, input, position, multiple, selected, value, error,
191
+ // isPristine, setValue(raw),
191
192
  // options: [{ value, label, hotkey, isSelected, isDefault, select() }] }],
192
193
  // answer: { available, isSubmitting, run() }, skip: { … } }
193
194
  ```
194
195
 
196
+ **Typed questions.** A question with `input` asks for a value instead of a pick — `input.type` is
197
+ `text` · `textarea` · `number` · `boolean` · `date` · `email` · `url` · `select` (a pick from
198
+ `options` again), with optional `placeholder`, `required`, `min`, `max` and `pattern`. `options` may
199
+ be empty for one. Render the control from `input.type` and hand whatever it produces to
200
+ `question.setValue(raw)`: it is coerced with `coerceAnswer` to the canonical strings the wire
201
+ carries (numbers as decimals, booleans as `"true"`/`"false"`, dates as `YYYY-MM-DD`). `question.error`
202
+ is the server's own verdict on the current selection (`validateAnswer`), and `block.isValid` is all
203
+ of them — the answer route answers `400` with the same message for anything it would refuse.
204
+
205
+ ```tsx
206
+ {q.input?.type === 'number' ? (
207
+ <input type="number" value={q.value} placeholder={q.input.placeholder}
208
+ min={q.input.min} max={q.input.max} onChange={(e) => q.setValue(e.target.value)} />
209
+ ) : q.input?.type === 'boolean' ? (
210
+ <input type="checkbox" checked={q.value === 'true'} onChange={(e) => q.setValue(e.target.checked)} />
211
+ ) : null}
212
+ {q.error && !q.isPristine ? <small role="alert">{q.error}</small> : null}
213
+ ```
214
+
195
215
  Every option the agent pre-picked comes back as `isDefault` and starts `isSelected`, so confirming
196
216
  is enough. `answer.run()` submits **only the questions the user touched** — the rest are omitted on
197
217
  purpose, so the server applies the same defaults it showed and the settled row still records which
package/dist/index.cjs CHANGED
@@ -44,6 +44,7 @@ __export(src_exports, {
44
44
  approvalCountdown: () => approvalCountdown,
45
45
  backgroundRunsFromThread: () => backgroundRunsFromThread,
46
46
  buildTranscriptBlocks: () => buildTranscriptBlocks,
47
+ coerceAnswer: () => coerceAnswer,
47
48
  correctedCallIds: () => correctedCallIds,
48
49
  createSkillsSource: () => createSkillsSource,
49
50
  describeTimestamp: () => describeTimestamp,
@@ -76,7 +77,9 @@ __export(src_exports, {
76
77
  useSpeechRecognition: () => useSpeechRecognition,
77
78
  useStickToBottom: () => useStickToBottom,
78
79
  useToolCatalog: () => useToolCatalog,
79
- useTranscriptItem: () => useTranscriptItem
80
+ useTranscriptItem: () => useTranscriptItem,
81
+ validateAnswer: () => import_nestjs_agent_core.validateElicitationAnswer,
82
+ validateAnswerValue: () => import_nestjs_agent_core.validateElicitationValue
80
83
  });
81
84
  module.exports = __toCommonJS(src_exports);
82
85
 
@@ -1057,8 +1060,66 @@ var import_react6 = __toESM(require("react"), 1);
1057
1060
  var import_react5 = require("react");
1058
1061
 
1059
1062
  // src/transcript/model.ts
1063
+ var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
1060
1064
  var import_ai2 = require("ai");
1061
1065
 
1066
+ // src/elicitation/answers.ts
1067
+ var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
1068
+ function pad(value) {
1069
+ return String(value).padStart(2, "0");
1070
+ }
1071
+ __name(pad, "pad");
1072
+ function toDay(date) {
1073
+ if (Number.isNaN(date.getTime())) {
1074
+ return null;
1075
+ }
1076
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1077
+ }
1078
+ __name(toDay, "toDay");
1079
+ function coerceOne(type, raw) {
1080
+ if (raw === null || raw === void 0) {
1081
+ return null;
1082
+ }
1083
+ if (Array.isArray(raw)) {
1084
+ return null;
1085
+ }
1086
+ if (raw instanceof Date) {
1087
+ return type === "date" || type === void 0 ? toDay(raw) : raw.toISOString();
1088
+ }
1089
+ if (typeof raw === "boolean") {
1090
+ return raw ? "true" : "false";
1091
+ }
1092
+ if (typeof raw === "number") {
1093
+ return Number.isFinite(raw) ? String(raw) : null;
1094
+ }
1095
+ const text = raw;
1096
+ switch (type) {
1097
+ case "number":
1098
+ case "email":
1099
+ case "url":
1100
+ case "date":
1101
+ return text.trim() === "" ? null : text.trim();
1102
+ case "boolean": {
1103
+ const lowered = text.trim().toLowerCase();
1104
+ if (lowered === "true" || lowered === "on" || lowered === "yes") return "true";
1105
+ if (lowered === "false" || lowered === "off" || lowered === "no") return "false";
1106
+ return lowered === "" ? null : text;
1107
+ }
1108
+ default:
1109
+ return text === "" ? null : text;
1110
+ }
1111
+ }
1112
+ __name(coerceOne, "coerceOne");
1113
+ function coerceAnswer(question, raw) {
1114
+ const type = question.input?.type;
1115
+ const items = Array.isArray(raw) ? raw : [
1116
+ raw
1117
+ ];
1118
+ const values = items.map((item) => coerceOne(type, item)).filter((value) => value !== null);
1119
+ return question.multiple === true ? values : values.slice(0, 1);
1120
+ }
1121
+ __name(coerceAnswer, "coerceAnswer");
1122
+
1062
1123
  // src/presentation/tool-activity.ts
1063
1124
  var import_ai = require("ai");
1064
1125
 
@@ -1595,11 +1656,16 @@ function readElicitationRequest(part) {
1595
1656
  }
1596
1657
  const questions = [];
1597
1658
  for (const candidate of input.questions) {
1598
- if (!isRecord2(candidate) || typeof candidate.id !== "string" || typeof candidate.prompt !== "string" || !Array.isArray(candidate.options) || candidate.options.length === 0) {
1659
+ if (!isRecord2(candidate) || typeof candidate.id !== "string" || typeof candidate.prompt !== "string") {
1660
+ return null;
1661
+ }
1662
+ const input2 = (0, import_nestjs_agent_core2.readElicitationInput)(candidate.input) ?? null;
1663
+ const rawOptions = Array.isArray(candidate.options) ? candidate.options : [];
1664
+ if (rawOptions.length === 0 && (input2 === null || input2.type === "select")) {
1599
1665
  return null;
1600
1666
  }
1601
1667
  const options = [];
1602
- for (const option of candidate.options) {
1668
+ for (const option of rawOptions) {
1603
1669
  if (!isRecord2(option) || typeof option.value !== "string" || typeof option.label !== "string") {
1604
1670
  return null;
1605
1671
  }
@@ -1612,7 +1678,10 @@ function readElicitationRequest(part) {
1612
1678
  questions.push({
1613
1679
  id: candidate.id,
1614
1680
  prompt: candidate.prompt,
1681
+ description: typeof candidate.description === "string" ? candidate.description : null,
1682
+ input: input2,
1615
1683
  multiple: candidate.multiple === true,
1684
+ allowFreeText: candidate.allowFreeText === true,
1616
1685
  defaults: Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [],
1617
1686
  options
1618
1687
  });
@@ -1648,12 +1717,38 @@ function buildElicitationBlock(key, part, request, options) {
1648
1717
  const picked = isPending ? options.picked(toolCallId, question.id) : void 0;
1649
1718
  const selected = picked ?? outcome?.answers[question.id] ?? question.defaults;
1650
1719
  const pick = /* @__PURE__ */ __name((values) => options.pick(toolCallId, question.id, values), "pick");
1720
+ const error = isPending ? (0, import_nestjs_agent_core2.validateElicitationAnswer)({
1721
+ id: question.id,
1722
+ prompt: question.prompt,
1723
+ options: question.options.map(({ value, label }) => ({
1724
+ value,
1725
+ label
1726
+ })),
1727
+ ...question.input !== null ? {
1728
+ input: question.input
1729
+ } : {},
1730
+ ...question.multiple ? {
1731
+ multiple: true
1732
+ } : {},
1733
+ ...question.allowFreeText ? {
1734
+ allowFreeText: true
1735
+ } : {}
1736
+ }, selected) : null;
1651
1737
  return {
1652
1738
  id: question.id,
1653
1739
  prompt: question.prompt,
1740
+ description: question.description,
1741
+ input: question.input,
1654
1742
  multiple: question.multiple,
1655
1743
  position: index + 1,
1656
1744
  selected,
1745
+ value: selected[0] ?? "",
1746
+ error,
1747
+ setValue: /* @__PURE__ */ __name((raw) => {
1748
+ if (isPending) {
1749
+ pick(coerceAnswer(question, raw));
1750
+ }
1751
+ }, "setValue"),
1657
1752
  isPristine: isPending ? picked === void 0 : outcome?.defaulted.includes(question.id) ?? true,
1658
1753
  options: question.options.map((option) => ({
1659
1754
  value: option.value,
@@ -1688,6 +1783,7 @@ function buildElicitationBlock(key, part, request, options) {
1688
1783
  questions,
1689
1784
  questionCount: questions.length,
1690
1785
  isPending,
1786
+ isValid: questions.every((question) => question.error === null),
1691
1787
  outcome,
1692
1788
  error: options.errorOf(toolCallId),
1693
1789
  answer: {
@@ -3196,7 +3292,7 @@ function useApprovalCountdown(expiresAt, options = {}) {
3196
3292
  __name(useApprovalCountdown, "useApprovalCountdown");
3197
3293
 
3198
3294
  // src/stored-message-to-ui-message.ts
3199
- var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
3295
+ var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
3200
3296
  function isRefusal(result) {
3201
3297
  if (result.denied === true) {
3202
3298
  return true;
@@ -3209,7 +3305,7 @@ function refusalReason(result) {
3209
3305
  const { output } = result;
3210
3306
  if (output !== null && typeof output === "object" && "reason" in output) {
3211
3307
  const reason = output.reason;
3212
- return typeof reason === "string" && reason !== import_nestjs_agent_core.DEFAULT_REFUSAL_REASON ? reason : void 0;
3308
+ return typeof reason === "string" && reason !== import_nestjs_agent_core3.DEFAULT_REFUSAL_REASON ? reason : void 0;
3213
3309
  }
3214
3310
  return void 0;
3215
3311
  }
@@ -3757,6 +3853,7 @@ __name(mergeHeaders, "mergeHeaders");
3757
3853
  approvalCountdown,
3758
3854
  backgroundRunsFromThread,
3759
3855
  buildTranscriptBlocks,
3856
+ coerceAnswer,
3760
3857
  correctedCallIds,
3761
3858
  createSkillsSource,
3762
3859
  describeTimestamp,
@@ -3789,6 +3886,8 @@ __name(mergeHeaders, "mergeHeaders");
3789
3886
  useSpeechRecognition,
3790
3887
  useStickToBottom,
3791
3888
  useToolCatalog,
3792
- useTranscriptItem
3889
+ useTranscriptItem,
3890
+ validateAnswer,
3891
+ validateAnswerValue
3793
3892
  });
3794
3893
  //# sourceMappingURL=index.cjs.map