@xinghunm/ai-chat 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -102,11 +102,28 @@ interface PlanBooleanQuestion extends PlanQuestionBase {
102
102
  */
103
103
  type PlanQuestion = PlanSingleSelectQuestion | PlanMultiSelectQuestion | PlanTextQuestion | PlanNumberQuestion | PlanBooleanQuestion;
104
104
  type PlanQuestionnaireStatus = 'expired' | 'failed';
105
+ /**
106
+ * Merge behavior applied when a keyed structured block is patched repeatedly.
107
+ */
108
+ type ChatBlockMergePolicy = 'append' | 'replace' | 'ignore-duplicate';
105
109
  /**
106
110
  * Structured question form emitted for plan-mode clarification flows.
107
111
  */
108
112
  interface PlanQuestionnaire {
109
113
  questionnaireId: string;
114
+ /**
115
+ * Stable key used to identify the same logical questionnaire across streaming patches.
116
+ * When present, questionnaires are merged by `blockKey` instead of `questionnaireId`.
117
+ */
118
+ blockKey?: string;
119
+ /**
120
+ * Merge strategy used when another questionnaire with the same key arrives.
121
+ *
122
+ * - `append`: keep appending questionnaires even if the key matches.
123
+ * - `replace`: replace the existing keyed questionnaire with the latest one.
124
+ * - `ignore-duplicate`: keep the first keyed questionnaire and ignore later duplicates.
125
+ */
126
+ mergePolicy?: ChatBlockMergePolicy;
110
127
  title?: string;
111
128
  description?: string;
112
129
  submitLabel?: string;
@@ -120,6 +137,10 @@ interface PlanQuestionnaire {
120
137
  */
121
138
  interface PlanQuestionnaireSubmission {
122
139
  questionnaireId: string;
140
+ /**
141
+ * Stable questionnaire block key forwarded from the rendered card when available.
142
+ */
143
+ blockKey?: string;
123
144
  answers: Record<string, PlanQuestionnaireAnswerValue>;
124
145
  content: string;
125
146
  sourceMessageId?: string;
@@ -168,6 +189,27 @@ interface ResultSummary {
168
189
  headline: string;
169
190
  details: string[];
170
191
  }
192
+ /**
193
+ * Extensible custom block rendered by a consumer-provided block renderer.
194
+ */
195
+ interface ChatCustomBlock {
196
+ type: 'custom';
197
+ kind: string;
198
+ data: unknown;
199
+ /**
200
+ * Stable key used to identify the same logical block across streaming patches.
201
+ * When present, keyed custom blocks are merged by `blockKey` regardless of `kind`.
202
+ */
203
+ blockKey?: string;
204
+ /**
205
+ * Merge strategy used when another custom block with the same key arrives.
206
+ *
207
+ * - `append`: keep appending blocks even if the key matches.
208
+ * - `replace`: replace the existing keyed block with the latest one.
209
+ * - `ignore-duplicate`: keep the first keyed block and ignore later duplicates.
210
+ */
211
+ mergePolicy?: ChatBlockMergePolicy;
212
+ }
171
213
  /**
172
214
  * Structured assistant block variants rendered by the chat thread.
173
215
  */
@@ -190,11 +232,7 @@ type ChatMessageBlock = {
190
232
  } | {
191
233
  type: 'questionnaire';
192
234
  questionnaire: PlanQuestionnaire;
193
- } | {
194
- type: 'custom';
195
- kind: string;
196
- data: unknown;
197
- };
235
+ } | ChatCustomBlock;
198
236
  /**
199
237
  * Supported message body render orders for mixed text and structured blocks.
200
238
  */
package/dist/index.d.ts CHANGED
@@ -102,11 +102,28 @@ interface PlanBooleanQuestion extends PlanQuestionBase {
102
102
  */
103
103
  type PlanQuestion = PlanSingleSelectQuestion | PlanMultiSelectQuestion | PlanTextQuestion | PlanNumberQuestion | PlanBooleanQuestion;
104
104
  type PlanQuestionnaireStatus = 'expired' | 'failed';
105
+ /**
106
+ * Merge behavior applied when a keyed structured block is patched repeatedly.
107
+ */
108
+ type ChatBlockMergePolicy = 'append' | 'replace' | 'ignore-duplicate';
105
109
  /**
106
110
  * Structured question form emitted for plan-mode clarification flows.
107
111
  */
108
112
  interface PlanQuestionnaire {
109
113
  questionnaireId: string;
114
+ /**
115
+ * Stable key used to identify the same logical questionnaire across streaming patches.
116
+ * When present, questionnaires are merged by `blockKey` instead of `questionnaireId`.
117
+ */
118
+ blockKey?: string;
119
+ /**
120
+ * Merge strategy used when another questionnaire with the same key arrives.
121
+ *
122
+ * - `append`: keep appending questionnaires even if the key matches.
123
+ * - `replace`: replace the existing keyed questionnaire with the latest one.
124
+ * - `ignore-duplicate`: keep the first keyed questionnaire and ignore later duplicates.
125
+ */
126
+ mergePolicy?: ChatBlockMergePolicy;
110
127
  title?: string;
111
128
  description?: string;
112
129
  submitLabel?: string;
@@ -120,6 +137,10 @@ interface PlanQuestionnaire {
120
137
  */
121
138
  interface PlanQuestionnaireSubmission {
122
139
  questionnaireId: string;
140
+ /**
141
+ * Stable questionnaire block key forwarded from the rendered card when available.
142
+ */
143
+ blockKey?: string;
123
144
  answers: Record<string, PlanQuestionnaireAnswerValue>;
124
145
  content: string;
125
146
  sourceMessageId?: string;
@@ -168,6 +189,27 @@ interface ResultSummary {
168
189
  headline: string;
169
190
  details: string[];
170
191
  }
192
+ /**
193
+ * Extensible custom block rendered by a consumer-provided block renderer.
194
+ */
195
+ interface ChatCustomBlock {
196
+ type: 'custom';
197
+ kind: string;
198
+ data: unknown;
199
+ /**
200
+ * Stable key used to identify the same logical block across streaming patches.
201
+ * When present, keyed custom blocks are merged by `blockKey` regardless of `kind`.
202
+ */
203
+ blockKey?: string;
204
+ /**
205
+ * Merge strategy used when another custom block with the same key arrives.
206
+ *
207
+ * - `append`: keep appending blocks even if the key matches.
208
+ * - `replace`: replace the existing keyed block with the latest one.
209
+ * - `ignore-duplicate`: keep the first keyed block and ignore later duplicates.
210
+ */
211
+ mergePolicy?: ChatBlockMergePolicy;
212
+ }
171
213
  /**
172
214
  * Structured assistant block variants rendered by the chat thread.
173
215
  */
@@ -190,11 +232,7 @@ type ChatMessageBlock = {
190
232
  } | {
191
233
  type: 'questionnaire';
192
234
  questionnaire: PlanQuestionnaire;
193
- } | {
194
- type: 'custom';
195
- kind: string;
196
- data: unknown;
197
- };
235
+ } | ChatCustomBlock;
198
236
  /**
199
237
  * Supported message body render orders for mixed text and structured blocks.
200
238
  */
package/dist/index.js CHANGED
@@ -108,6 +108,38 @@ var resolveSessionTitleFromMessage = (message) => {
108
108
  var mergeStreamingBlocks = (existingBlocks, incomingBlocks) => {
109
109
  const nextBlocks = [...existingBlocks ?? []];
110
110
  incomingBlocks.forEach((incomingBlock) => {
111
+ if (incomingBlock.type === "custom" && incomingBlock.blockKey) {
112
+ const mergePolicy = incomingBlock.mergePolicy ?? "append";
113
+ if (mergePolicy !== "append") {
114
+ const existingIndex2 = nextBlocks.findIndex(
115
+ (block) => block.type === "custom" && block.blockKey === incomingBlock.blockKey
116
+ );
117
+ if (existingIndex2 !== -1) {
118
+ if (mergePolicy === "replace") {
119
+ nextBlocks[existingIndex2] = incomingBlock;
120
+ }
121
+ return;
122
+ }
123
+ }
124
+ nextBlocks.push(incomingBlock);
125
+ return;
126
+ }
127
+ if (incomingBlock.type === "questionnaire" && incomingBlock.questionnaire.blockKey) {
128
+ const mergePolicy = incomingBlock.questionnaire.mergePolicy ?? "append";
129
+ if (mergePolicy !== "append") {
130
+ const existingIndex2 = nextBlocks.findIndex(
131
+ (block) => block.type === "questionnaire" && block.questionnaire.blockKey === incomingBlock.questionnaire.blockKey
132
+ );
133
+ if (existingIndex2 !== -1) {
134
+ if (mergePolicy === "replace") {
135
+ nextBlocks[existingIndex2] = incomingBlock;
136
+ }
137
+ return;
138
+ }
139
+ }
140
+ nextBlocks.push(incomingBlock);
141
+ return;
142
+ }
111
143
  if (incomingBlock.type !== "questionnaire") {
112
144
  nextBlocks.push(incomingBlock);
113
145
  return;
@@ -1008,9 +1040,9 @@ var getTimelineBlockKey = (block, index) => {
1008
1040
  case "result_summary":
1009
1041
  return `${index}:result_summary:${block.summary.summaryId}:${block.summary.status}`;
1010
1042
  case "questionnaire":
1011
- return `${index}:questionnaire:${block.questionnaire.questionnaireId}`;
1043
+ return block.questionnaire.blockKey ? `questionnaire:${block.questionnaire.blockKey}` : `${index}:questionnaire:${block.questionnaire.questionnaireId}`;
1012
1044
  case "custom":
1013
- return `${index}:custom:${block.kind}:${stringifyTimelineKeyPart(block.data)}`;
1045
+ return block.blockKey ? `custom:${block.blockKey}` : `${index}:custom:${block.kind}:${stringifyTimelineKeyPart(block.data)}`;
1014
1046
  default:
1015
1047
  return null;
1016
1048
  }
@@ -1656,6 +1688,7 @@ var QuestionnaireCardInner = ({
1656
1688
  try {
1657
1689
  await onSubmit?.({
1658
1690
  questionnaireId: questionnaire.questionnaireId,
1691
+ ...questionnaire.blockKey ? { blockKey: questionnaire.blockKey } : {},
1659
1692
  answers: normalizedAnswers,
1660
1693
  content: contentLines.join("\n")
1661
1694
  });
@@ -2467,7 +2500,7 @@ var ChatMessageItemView = ({
2467
2500
  sourceMessageId: message.id
2468
2501
  }) : void 0
2469
2502
  }
2470
- ) }, `questionnaire-${index}`);
2503
+ ) }, block.questionnaire.blockKey ?? `questionnaire-${index}`);
2471
2504
  case "custom":
2472
2505
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react9.Fragment, { children: renderMessageBlock?.({
2473
2506
  block,
package/dist/index.mjs CHANGED
@@ -61,6 +61,38 @@ var resolveSessionTitleFromMessage = (message) => {
61
61
  var mergeStreamingBlocks = (existingBlocks, incomingBlocks) => {
62
62
  const nextBlocks = [...existingBlocks ?? []];
63
63
  incomingBlocks.forEach((incomingBlock) => {
64
+ if (incomingBlock.type === "custom" && incomingBlock.blockKey) {
65
+ const mergePolicy = incomingBlock.mergePolicy ?? "append";
66
+ if (mergePolicy !== "append") {
67
+ const existingIndex2 = nextBlocks.findIndex(
68
+ (block) => block.type === "custom" && block.blockKey === incomingBlock.blockKey
69
+ );
70
+ if (existingIndex2 !== -1) {
71
+ if (mergePolicy === "replace") {
72
+ nextBlocks[existingIndex2] = incomingBlock;
73
+ }
74
+ return;
75
+ }
76
+ }
77
+ nextBlocks.push(incomingBlock);
78
+ return;
79
+ }
80
+ if (incomingBlock.type === "questionnaire" && incomingBlock.questionnaire.blockKey) {
81
+ const mergePolicy = incomingBlock.questionnaire.mergePolicy ?? "append";
82
+ if (mergePolicy !== "append") {
83
+ const existingIndex2 = nextBlocks.findIndex(
84
+ (block) => block.type === "questionnaire" && block.questionnaire.blockKey === incomingBlock.questionnaire.blockKey
85
+ );
86
+ if (existingIndex2 !== -1) {
87
+ if (mergePolicy === "replace") {
88
+ nextBlocks[existingIndex2] = incomingBlock;
89
+ }
90
+ return;
91
+ }
92
+ }
93
+ nextBlocks.push(incomingBlock);
94
+ return;
95
+ }
64
96
  if (incomingBlock.type !== "questionnaire") {
65
97
  nextBlocks.push(incomingBlock);
66
98
  return;
@@ -961,9 +993,9 @@ var getTimelineBlockKey = (block, index) => {
961
993
  case "result_summary":
962
994
  return `${index}:result_summary:${block.summary.summaryId}:${block.summary.status}`;
963
995
  case "questionnaire":
964
- return `${index}:questionnaire:${block.questionnaire.questionnaireId}`;
996
+ return block.questionnaire.blockKey ? `questionnaire:${block.questionnaire.blockKey}` : `${index}:questionnaire:${block.questionnaire.questionnaireId}`;
965
997
  case "custom":
966
- return `${index}:custom:${block.kind}:${stringifyTimelineKeyPart(block.data)}`;
998
+ return block.blockKey ? `custom:${block.blockKey}` : `${index}:custom:${block.kind}:${stringifyTimelineKeyPart(block.data)}`;
967
999
  default:
968
1000
  return null;
969
1001
  }
@@ -1609,6 +1641,7 @@ var QuestionnaireCardInner = ({
1609
1641
  try {
1610
1642
  await onSubmit?.({
1611
1643
  questionnaireId: questionnaire.questionnaireId,
1644
+ ...questionnaire.blockKey ? { blockKey: questionnaire.blockKey } : {},
1612
1645
  answers: normalizedAnswers,
1613
1646
  content: contentLines.join("\n")
1614
1647
  });
@@ -2420,7 +2453,7 @@ var ChatMessageItemView = ({
2420
2453
  sourceMessageId: message.id
2421
2454
  }) : void 0
2422
2455
  }
2423
- ) }, `questionnaire-${index}`);
2456
+ ) }, block.questionnaire.blockKey ?? `questionnaire-${index}`);
2424
2457
  case "custom":
2425
2458
  return /* @__PURE__ */ jsx8(Fragment, { children: renderMessageBlock?.({
2426
2459
  block,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xinghunm/ai-chat",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AI chat React component library",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",