@dudousxd/nestjs-agent-react 0.11.0 → 0.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.
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
 
@@ -539,6 +542,9 @@ var AgentChatTransport = class {
539
542
  props: event.props,
540
543
  ...event.version !== void 0 ? {
541
544
  version: event.version
545
+ } : {},
546
+ ...event.toolCallId !== void 0 ? {
547
+ toolCallId: event.toolCallId
542
548
  } : {}
543
549
  }
544
550
  });
@@ -1057,8 +1063,66 @@ var import_react6 = __toESM(require("react"), 1);
1057
1063
  var import_react5 = require("react");
1058
1064
 
1059
1065
  // src/transcript/model.ts
1066
+ var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
1060
1067
  var import_ai2 = require("ai");
1061
1068
 
1069
+ // src/elicitation/answers.ts
1070
+ var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
1071
+ function pad(value) {
1072
+ return String(value).padStart(2, "0");
1073
+ }
1074
+ __name(pad, "pad");
1075
+ function toDay(date) {
1076
+ if (Number.isNaN(date.getTime())) {
1077
+ return null;
1078
+ }
1079
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1080
+ }
1081
+ __name(toDay, "toDay");
1082
+ function coerceOne(type, raw) {
1083
+ if (raw === null || raw === void 0) {
1084
+ return null;
1085
+ }
1086
+ if (Array.isArray(raw)) {
1087
+ return null;
1088
+ }
1089
+ if (raw instanceof Date) {
1090
+ return type === "date" || type === void 0 ? toDay(raw) : raw.toISOString();
1091
+ }
1092
+ if (typeof raw === "boolean") {
1093
+ return raw ? "true" : "false";
1094
+ }
1095
+ if (typeof raw === "number") {
1096
+ return Number.isFinite(raw) ? String(raw) : null;
1097
+ }
1098
+ const text = raw;
1099
+ switch (type) {
1100
+ case "number":
1101
+ case "email":
1102
+ case "url":
1103
+ case "date":
1104
+ return text.trim() === "" ? null : text.trim();
1105
+ case "boolean": {
1106
+ const lowered = text.trim().toLowerCase();
1107
+ if (lowered === "true" || lowered === "on" || lowered === "yes") return "true";
1108
+ if (lowered === "false" || lowered === "off" || lowered === "no") return "false";
1109
+ return lowered === "" ? null : text;
1110
+ }
1111
+ default:
1112
+ return text === "" ? null : text;
1113
+ }
1114
+ }
1115
+ __name(coerceOne, "coerceOne");
1116
+ function coerceAnswer(question, raw) {
1117
+ const type = question.input?.type;
1118
+ const items = Array.isArray(raw) ? raw : [
1119
+ raw
1120
+ ];
1121
+ const values = items.map((item) => coerceOne(type, item)).filter((value) => value !== null);
1122
+ return question.multiple === true ? values : values.slice(0, 1);
1123
+ }
1124
+ __name(coerceAnswer, "coerceAnswer");
1125
+
1062
1126
  // src/presentation/tool-activity.ts
1063
1127
  var import_ai = require("ai");
1064
1128
 
@@ -1595,11 +1659,16 @@ function readElicitationRequest(part) {
1595
1659
  }
1596
1660
  const questions = [];
1597
1661
  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) {
1662
+ if (!isRecord2(candidate) || typeof candidate.id !== "string" || typeof candidate.prompt !== "string") {
1663
+ return null;
1664
+ }
1665
+ const input2 = (0, import_nestjs_agent_core2.readElicitationInput)(candidate.input) ?? null;
1666
+ const rawOptions = Array.isArray(candidate.options) ? candidate.options : [];
1667
+ if (rawOptions.length === 0 && (input2 === null || input2.type === "select")) {
1599
1668
  return null;
1600
1669
  }
1601
1670
  const options = [];
1602
- for (const option of candidate.options) {
1671
+ for (const option of rawOptions) {
1603
1672
  if (!isRecord2(option) || typeof option.value !== "string" || typeof option.label !== "string") {
1604
1673
  return null;
1605
1674
  }
@@ -1612,7 +1681,10 @@ function readElicitationRequest(part) {
1612
1681
  questions.push({
1613
1682
  id: candidate.id,
1614
1683
  prompt: candidate.prompt,
1684
+ description: typeof candidate.description === "string" ? candidate.description : null,
1685
+ input: input2,
1615
1686
  multiple: candidate.multiple === true,
1687
+ allowFreeText: candidate.allowFreeText === true,
1616
1688
  defaults: Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [],
1617
1689
  options
1618
1690
  });
@@ -1648,12 +1720,38 @@ function buildElicitationBlock(key, part, request, options) {
1648
1720
  const picked = isPending ? options.picked(toolCallId, question.id) : void 0;
1649
1721
  const selected = picked ?? outcome?.answers[question.id] ?? question.defaults;
1650
1722
  const pick = /* @__PURE__ */ __name((values) => options.pick(toolCallId, question.id, values), "pick");
1723
+ const error = isPending ? (0, import_nestjs_agent_core2.validateElicitationAnswer)({
1724
+ id: question.id,
1725
+ prompt: question.prompt,
1726
+ options: question.options.map(({ value, label }) => ({
1727
+ value,
1728
+ label
1729
+ })),
1730
+ ...question.input !== null ? {
1731
+ input: question.input
1732
+ } : {},
1733
+ ...question.multiple ? {
1734
+ multiple: true
1735
+ } : {},
1736
+ ...question.allowFreeText ? {
1737
+ allowFreeText: true
1738
+ } : {}
1739
+ }, selected) : null;
1651
1740
  return {
1652
1741
  id: question.id,
1653
1742
  prompt: question.prompt,
1743
+ description: question.description,
1744
+ input: question.input,
1654
1745
  multiple: question.multiple,
1655
1746
  position: index + 1,
1656
1747
  selected,
1748
+ value: selected[0] ?? "",
1749
+ error,
1750
+ setValue: /* @__PURE__ */ __name((raw) => {
1751
+ if (isPending) {
1752
+ pick(coerceAnswer(question, raw));
1753
+ }
1754
+ }, "setValue"),
1657
1755
  isPristine: isPending ? picked === void 0 : outcome?.defaulted.includes(question.id) ?? true,
1658
1756
  options: question.options.map((option) => ({
1659
1757
  value: option.value,
@@ -1688,6 +1786,7 @@ function buildElicitationBlock(key, part, request, options) {
1688
1786
  questions,
1689
1787
  questionCount: questions.length,
1690
1788
  isPending,
1789
+ isValid: questions.every((question) => question.error === null),
1691
1790
  outcome,
1692
1791
  error: options.errorOf(toolCallId),
1693
1792
  answer: {
@@ -1802,7 +1901,8 @@ function readUiComponent(part) {
1802
1901
  id,
1803
1902
  component: data.component,
1804
1903
  props: isRecord2(data.props) ? data.props : {},
1805
- version: typeof data.version === "number" ? data.version : null
1904
+ version: typeof data.version === "number" ? data.version : null,
1905
+ toolCallId: typeof data.toolCallId === "string" ? data.toolCallId : null
1806
1906
  };
1807
1907
  }
1808
1908
  __name(readUiComponent, "readUiComponent");
@@ -3196,7 +3296,7 @@ function useApprovalCountdown(expiresAt, options = {}) {
3196
3296
  __name(useApprovalCountdown, "useApprovalCountdown");
3197
3297
 
3198
3298
  // src/stored-message-to-ui-message.ts
3199
- var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
3299
+ var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
3200
3300
  function isRefusal(result) {
3201
3301
  if (result.denied === true) {
3202
3302
  return true;
@@ -3209,7 +3309,7 @@ function refusalReason(result) {
3209
3309
  const { output } = result;
3210
3310
  if (output !== null && typeof output === "object" && "reason" in output) {
3211
3311
  const reason = output.reason;
3212
- return typeof reason === "string" && reason !== import_nestjs_agent_core.DEFAULT_REFUSAL_REASON ? reason : void 0;
3312
+ return typeof reason === "string" && reason !== import_nestjs_agent_core3.DEFAULT_REFUSAL_REASON ? reason : void 0;
3213
3313
  }
3214
3314
  return void 0;
3215
3315
  }
@@ -3240,8 +3340,10 @@ function storedMessageToUiMessage(message) {
3240
3340
  url: attachment.url
3241
3341
  });
3242
3342
  }
3343
+ const toolCallIds = new Set((message.toolCalls ?? []).map((call) => call.id));
3344
+ const pushedByCall = /* @__PURE__ */ new Map();
3243
3345
  for (const component of message.ui ?? []) {
3244
- parts.push({
3346
+ const part = {
3245
3347
  type: "data-ui",
3246
3348
  id: component.id,
3247
3349
  data: {
@@ -3250,9 +3352,19 @@ function storedMessageToUiMessage(message) {
3250
3352
  props: component.props,
3251
3353
  ...component.version !== void 0 ? {
3252
3354
  version: component.version
3355
+ } : {},
3356
+ ...component.toolCallId !== void 0 ? {
3357
+ toolCallId: component.toolCallId
3253
3358
  } : {}
3254
3359
  }
3255
- });
3360
+ };
3361
+ if (component.toolCallId !== void 0 && toolCallIds.has(component.toolCallId)) {
3362
+ const list = pushedByCall.get(component.toolCallId) ?? [];
3363
+ list.push(part);
3364
+ pushedByCall.set(component.toolCallId, list);
3365
+ } else {
3366
+ parts.push(part);
3367
+ }
3256
3368
  }
3257
3369
  for (const approval of message.approvals ?? []) {
3258
3370
  parts.push({
@@ -3323,6 +3435,7 @@ function storedMessageToUiMessage(message) {
3323
3435
  input: call.input
3324
3436
  }
3325
3437
  });
3438
+ parts.push(...pushedByCall.get(call.id) ?? []);
3326
3439
  }
3327
3440
  return {
3328
3441
  id: message.id,
@@ -3757,6 +3870,7 @@ __name(mergeHeaders, "mergeHeaders");
3757
3870
  approvalCountdown,
3758
3871
  backgroundRunsFromThread,
3759
3872
  buildTranscriptBlocks,
3873
+ coerceAnswer,
3760
3874
  correctedCallIds,
3761
3875
  createSkillsSource,
3762
3876
  describeTimestamp,
@@ -3789,6 +3903,8 @@ __name(mergeHeaders, "mergeHeaders");
3789
3903
  useSpeechRecognition,
3790
3904
  useStickToBottom,
3791
3905
  useToolCatalog,
3792
- useTranscriptItem
3906
+ useTranscriptItem,
3907
+ validateAnswer,
3908
+ validateAnswerValue
3793
3909
  });
3794
3910
  //# sourceMappingURL=index.cjs.map