@lowdefy/blocks-antd-x 5.3.0 → 5.4.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.
@@ -45,17 +45,25 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
45
45
  }
46
46
  return initial;
47
47
  });
48
+ // When no conversationId is supplied, mint a stable session id so every turn
49
+ // (including a welcome prompt sent before the app sets the prop) posts a
50
+ // consistent id. App-supplied ids remain authoritative.
51
+ const mintedIdRef = useRef(null);
52
+ if (!conversationId && !mintedIdRef.current) {
53
+ mintedIdRef.current = crypto.randomUUID();
54
+ }
55
+ const effectiveConversationId = conversationId ?? mintedIdRef.current;
48
56
  const urlQueryKey = JSON.stringify(urlQuery ?? null);
49
57
  const transport = useMemo(()=>createLowdefyChatTransport({
50
58
  pageId,
51
59
  agentId,
52
- conversationId,
60
+ conversationId: effectiveConversationId,
53
61
  urlQuery,
54
62
  sharedStateRef
55
63
  }), [
56
64
  pageId,
57
65
  agentId,
58
- conversationId,
66
+ effectiveConversationId,
59
67
  urlQueryKey
60
68
  ]);
61
69
  const bubbleListRef = useRef(null);
@@ -134,14 +142,14 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
134
142
  });
135
143
  // Clear messages when conversationId changes so the new conversation starts clean.
136
144
  // Developers load saved messages via the messages property if needed.
137
- const prevConversationIdRef = useRef(conversationId);
145
+ const prevConversationIdRef = useRef(effectiveConversationId);
138
146
  useEffect(()=>{
139
- if (conversationId !== prevConversationIdRef.current) {
140
- prevConversationIdRef.current = conversationId;
147
+ if (effectiveConversationId !== prevConversationIdRef.current) {
148
+ prevConversationIdRef.current = effectiveConversationId;
141
149
  setMessages([]);
142
150
  }
143
151
  }, [
144
- conversationId,
152
+ effectiveConversationId,
145
153
  setMessages
146
154
  ]);
147
155
  // Sync external messages when provided — undefined means "not provided" (no sync),
@@ -241,7 +249,8 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
241
249
  messages,
242
250
  status,
243
251
  methods,
244
- finishMetaRef
252
+ finishMetaRef,
253
+ conversationId: effectiveConversationId
245
254
  });
246
255
  const isEmpty = messages.length === 0;
247
256
  const isBusy = status === 'streaming' || status === 'submitted';
@@ -497,7 +506,8 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
497
506
  onFeedback: handleFeedback,
498
507
  onRegenerate: handleRegenerate,
499
508
  onDelete: handleDelete,
500
- onEditMessage: handleEditMessage
509
+ onEditMessage: handleEditMessage,
510
+ translate: methods.translate
501
511
  })), !isEmpty && activeSuggestions && activeSuggestions.length > 0 && !isBusy && /*#__PURE__*/ React.createElement("div", {
502
512
  style: {
503
513
  padding: '0 16px 8px'
@@ -557,7 +567,7 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
557
567
  }
558
568
  }), /*#__PURE__*/ React.createElement(Sender, {
559
569
  ref: senderRef,
560
- placeholder: sender?.placeholder ?? 'Type a message...',
570
+ placeholder: sender?.placeholder ?? methods.translate('agent.sender.placeholder'),
561
571
  submitType: sender?.submitType ?? 'enter',
562
572
  allowSpeech: sender?.allowSpeech ?? false,
563
573
  onSubmit: handleSend,
@@ -71,13 +71,23 @@ function RichCodeBlock({ renderMermaid, codeHighlighter }) {
71
71
  }, children));
72
72
  };
73
73
  }
74
- function summarizeToolOutput(output) {
75
- if (output === null || output === undefined) return 'Completed (no data)';
76
- if (Array.isArray(output)) return `Returned ${output.length} result${output.length === 1 ? '' : 's'}`;
74
+ function summarizeToolOutput(output, translate) {
75
+ if (output === null || output === undefined) return translate('agent.toolResult.completedNoData');
76
+ if (Array.isArray(output)) {
77
+ return translate('agent.toolResult.returnedCount', {
78
+ count: output.length
79
+ });
80
+ }
77
81
  if (typeof output === 'object') {
78
82
  const keys = Object.keys(output);
79
- if (keys.length <= 3) return `Returned: ${keys.join(', ')}`;
80
- return `Returned object with ${keys.length} fields`;
83
+ if (keys.length <= 3) {
84
+ return translate('agent.toolResult.returnedKeys', {
85
+ keys: keys.join(', ')
86
+ });
87
+ }
88
+ return translate('agent.toolResult.returnedFields', {
89
+ count: keys.length
90
+ });
81
91
  }
82
92
  if (typeof output === 'string') {
83
93
  return output.length > 80 ? `${output.substring(0, 80)}...` : output;
@@ -101,13 +111,13 @@ function normalizeActions(actions) {
101
111
  }
102
112
  return actions ?? {};
103
113
  }
104
- function BubbleActions({ actions, textContent, messageId, onFeedback, onRegenerate, onDelete }) {
114
+ function BubbleActions({ actions, textContent, messageId, onFeedback, onRegenerate, onDelete, translate }) {
105
115
  const normalized = normalizeActions(actions);
106
116
  const items = [];
107
117
  if (normalized.copy) {
108
118
  items.push({
109
119
  key: 'copy',
110
- label: 'Copy',
120
+ label: translate('agent.message.copy'),
111
121
  actionRender: ()=>/*#__PURE__*/ React.createElement(Actions.Copy, {
112
122
  text: textContent
113
123
  })
@@ -116,7 +126,7 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, onRegenera
116
126
  if (normalized.feedback) {
117
127
  items.push({
118
128
  key: 'feedback',
119
- label: 'Feedback',
129
+ label: translate('agent.message.feedback'),
120
130
  actionRender: ()=>/*#__PURE__*/ React.createElement(Actions.Feedback, {
121
131
  onChange: (rating)=>onFeedback?.({
122
132
  messageId,
@@ -129,7 +139,7 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, onRegenera
129
139
  items.push({
130
140
  key: 'regenerate',
131
141
  icon: /*#__PURE__*/ React.createElement(ReloadOutlined, null),
132
- label: 'Regenerate',
142
+ label: translate('agent.message.regenerate'),
133
143
  onItemClick: ()=>onRegenerate?.({
134
144
  messageId
135
145
  })
@@ -139,7 +149,7 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, onRegenera
139
149
  items.push({
140
150
  key: 'delete',
141
151
  icon: /*#__PURE__*/ React.createElement(DeleteOutlined, null),
142
- label: 'Delete',
152
+ label: translate('agent.message.delete'),
143
153
  danger: true,
144
154
  onItemClick: ()=>onDelete?.({
145
155
  messageId
@@ -151,7 +161,7 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, onRegenera
151
161
  items: items
152
162
  });
153
163
  }
154
- function SourcesDisplay({ sourceParts, config }) {
164
+ function SourcesDisplay({ sourceParts, config, translate }) {
155
165
  if (sourceParts.length === 0) return null;
156
166
  const items = sourceParts.map((source, i)=>({
157
167
  key: `source-${i}`,
@@ -161,12 +171,12 @@ function SourcesDisplay({ sourceParts, config }) {
161
171
  }));
162
172
  return /*#__PURE__*/ React.createElement(Sources, {
163
173
  items: items,
164
- title: "Sources",
174
+ title: translate('agent.toolResult.sourcesTitle'),
165
175
  inline: config?.sourcesDisplay?.inline ?? false,
166
176
  expandIconPosition: config?.sourcesDisplay?.expandIconPosition ?? 'end'
167
177
  });
168
178
  }
169
- function MessageBubble({ content, isStreaming, parts, config, addToolApprovalResponse, actions, messageId, onFeedback, onRegenerate, onDelete }) {
179
+ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalResponse, actions, messageId, onFeedback, onRegenerate, onDelete, translate }) {
170
180
  const showThoughtChain = config?.showThoughtChain !== false;
171
181
  const showReasoning = config?.showReasoning !== false;
172
182
  const reasoningDisplay = config?.reasoningDisplay ?? 'interleaved';
@@ -202,14 +212,16 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
202
212
  config: markdownConfig
203
213
  }, content), /*#__PURE__*/ React.createElement(SourcesDisplay, {
204
214
  sourceParts: sourceParts,
205
- config: config
215
+ config: config,
216
+ translate: translate
206
217
  }), showActions && /*#__PURE__*/ React.createElement(BubbleActions, {
207
218
  actions: normalizedActions,
208
219
  textContent: content,
209
220
  messageId: messageId,
210
221
  onFeedback: onFeedback,
211
222
  onRegenerate: onRegenerate,
212
- onDelete: onDelete
223
+ onDelete: onDelete,
224
+ translate: translate
213
225
  }));
214
226
  }
215
227
  // Build segments of consecutive same-type parts.
@@ -285,7 +297,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
285
297
  const isActiveReasoning = isStreaming && segment.parts.some((p)=>p.state === 'streaming');
286
298
  return /*#__PURE__*/ React.createElement(Think, {
287
299
  key: `reasoning-${idx}`,
288
- title: "Reasoning",
300
+ title: translate('agent.toolResult.reasoningTitle'),
289
301
  defaultExpanded: false,
290
302
  loading: isActiveReasoning,
291
303
  blink: isActiveReasoning
@@ -298,7 +310,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
298
310
  return {
299
311
  key: tool.toolCallId,
300
312
  title: tool.toolName,
301
- description: 'Tool execution was rejected',
313
+ description: translate('agent.toolResult.rejected'),
302
314
  status: 'error'
303
315
  };
304
316
  }
@@ -317,7 +329,8 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
317
329
  onReject: (id)=>addToolApprovalResponse?.({
318
330
  id,
319
331
  approved: false
320
- })
332
+ }),
333
+ translate: translate
321
334
  }),
322
335
  status: 'loading'
323
336
  };
@@ -339,12 +352,12 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
339
352
  if (showInput && tool.input && Object.keys(tool.input).length > 0) {
340
353
  description = `Input: ${JSON.stringify(tool.input, null, 2)}`;
341
354
  } else {
342
- description = 'Running...';
355
+ description = translate('agent.toolResult.running');
343
356
  }
344
357
  } else if (status === 'error') {
345
- description = 'Tool execution failed';
358
+ description = translate('agent.toolResult.failed');
346
359
  } else if (toolOutput?.display && typeof toolOutput.display === 'string') {
347
- description = summarizeToolOutput(toolOutput.display);
360
+ description = summarizeToolOutput(toolOutput.display, translate);
348
361
  content = /*#__PURE__*/ React.createElement(Markdown, {
349
362
  components: markdownComponents,
350
363
  config: markdownConfig
@@ -352,7 +365,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
352
365
  collapsible = true;
353
366
  } else if (isSubAgent) {
354
367
  // Sub-agent results: short status in description, full response in styled content
355
- description = 'Completed';
368
+ description = translate('agent.toolResult.completed');
356
369
  const subAgentText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput, null, 2);
357
370
  content = /*#__PURE__*/ React.createElement("div", {
358
371
  style: {
@@ -369,19 +382,19 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
369
382
  } else {
370
383
  const mode = resolveToolResultMode(toolResultDisplay, tool.toolName);
371
384
  if (mode === 'readable') {
372
- description = summarizeToolOutput(toolOutput);
373
- content = formatToolResult(toolOutput);
385
+ description = summarizeToolOutput(toolOutput, translate);
386
+ content = formatToolResult(toolOutput, translate);
374
387
  collapsible = true;
375
388
  } else if (mode === 'full') {
376
- description = summarizeToolOutput(toolOutput);
389
+ description = summarizeToolOutput(toolOutput, translate);
377
390
  content = JSON.stringify(toolOutput, null, 2);
378
391
  collapsible = true;
379
392
  } else if (mode === 'none') {
380
- description = 'Completed';
393
+ description = translate('agent.toolResult.completed');
381
394
  } else {
382
395
  // summary mode: show summary, add readable content behind collapse
383
- description = summarizeToolOutput(toolOutput);
384
- const readable = formatToolResult(toolOutput);
396
+ description = summarizeToolOutput(toolOutput, translate);
397
+ const readable = formatToolResult(toolOutput, translate);
385
398
  if (readable != null) {
386
399
  content = readable;
387
400
  collapsible = true;
@@ -453,7 +466,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
453
466
  animation: 'spin 1s linear infinite',
454
467
  display: 'inline-block'
455
468
  }
456
- }, "⟳"), lastStatus.data?.message ?? 'Processing...');
469
+ }, "⟳"), lastStatus.data?.message ?? translate('agent.toolResult.processing'));
457
470
  }
458
471
  if (segment.category === 'text') {
459
472
  const text = segment.parts.map((p)=>p.text).join('');
@@ -471,14 +484,16 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
471
484
  return null;
472
485
  }), /*#__PURE__*/ React.createElement(SourcesDisplay, {
473
486
  sourceParts: sourceParts,
474
- config: config
487
+ config: config,
488
+ translate: translate
475
489
  }), showActions && /*#__PURE__*/ React.createElement(BubbleActions, {
476
490
  actions: normalizedActions,
477
491
  textContent: allTextContent,
478
492
  messageId: messageId,
479
493
  onFeedback: onFeedback,
480
494
  onRegenerate: onRegenerate,
481
- onDelete: onDelete
495
+ onDelete: onDelete,
496
+ translate: translate
482
497
  }));
483
498
  }
484
499
  export default MessageBubble;
@@ -31,8 +31,8 @@ function roleAvatar(roleConfig, fallbackIcon) {
31
31
  icon: fallbackIcon
32
32
  });
33
33
  }
34
- function roleHeader(roleConfig, fallback) {
35
- const name = roleConfig?.name ?? fallback;
34
+ function roleHeader(roleConfig, fallbackKey, translate) {
35
+ const name = roleConfig?.name ?? translate(fallbackKey);
36
36
  return /*#__PURE__*/ React.createElement("h5", {
37
37
  style: {
38
38
  margin: 0
@@ -55,7 +55,7 @@ function ThinkingBubbleContent({ bubbleId, config }) {
55
55
  label: label
56
56
  });
57
57
  }
58
- const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messages, isStreaming, config, addToolApprovalResponse, onFeedback, onRegenerate, onDelete, onEditMessage }, ref) {
58
+ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messages, isStreaming, config, addToolApprovalResponse, onFeedback, onRegenerate, onDelete, onEditMessage, translate }, ref) {
59
59
  // Build a lookup map for message parts.
60
60
  // Bubble.List's contentRender callback only receives (content, info) where info.key is the
61
61
  // item key — it does not receive the full message object with its parts array. This map
@@ -128,7 +128,7 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
128
128
  variant: config?.roles?.user?.variant ?? 'filled',
129
129
  shape: config?.roles?.user?.shape ?? 'round',
130
130
  avatar: roleAvatar(config?.roles?.user, /*#__PURE__*/ React.createElement(UserOutlined, null)),
131
- header: roleHeader(config?.roles?.user, 'You'),
131
+ header: roleHeader(config?.roles?.user, 'agent.message.userHeader', translate),
132
132
  editable: config?.editableMessages !== false ? {
133
133
  onEditConfirm: (key, newContent)=>{
134
134
  const parts = partsMap.get(key);
@@ -174,7 +174,7 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
174
174
  maxWidth: '100%'
175
175
  },
176
176
  avatar: roleAvatar(config?.roles?.assistant, /*#__PURE__*/ React.createElement(RobotOutlined, null)),
177
- header: roleHeader(config?.roles?.assistant, 'Assistant'),
177
+ header: roleHeader(config?.roles?.assistant, 'agent.message.assistantHeader', translate),
178
178
  typing: config?.roles?.assistant?.typing ? config.roles.assistant.typing : {
179
179
  effect: 'fade-in'
180
180
  },
@@ -190,7 +190,8 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
190
190
  messageId: info.key,
191
191
  onFeedback: onFeedback,
192
192
  onRegenerate: onRegenerate,
193
- onDelete: onDelete
193
+ onDelete: onDelete,
194
+ translate: translate
194
195
  });
195
196
  }
196
197
  }
@@ -14,7 +14,7 @@
14
14
  limitations under the License.
15
15
  */ import React from 'react';
16
16
  import { Button, Space } from 'antd';
17
- function ToolApproval({ toolName, input, approvalId, onApprove, onReject }) {
17
+ function ToolApproval({ toolName, input, approvalId, onApprove, onReject, translate }) {
18
18
  return /*#__PURE__*/ React.createElement("div", {
19
19
  style: {
20
20
  padding: '8px 0'
@@ -37,9 +37,9 @@ function ToolApproval({ toolName, input, approvalId, onApprove, onReject }) {
37
37
  type: "primary",
38
38
  size: "small",
39
39
  onClick: ()=>onApprove(approvalId)
40
- }, "Approve"), /*#__PURE__*/ React.createElement(Button, {
40
+ }, translate('agent.toolApproval.approve')), /*#__PURE__*/ React.createElement(Button, {
41
41
  size: "small",
42
42
  onClick: ()=>onReject(approvalId)
43
- }, "Reject")));
43
+ }, translate('agent.toolApproval.reject'))));
44
44
  }
45
45
  export default ToolApproval;
@@ -24,7 +24,7 @@ function isIsoDate(value) {
24
24
  function humanizeKey(key) {
25
25
  return key.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/[_-]/g, ' ').replace(/^\w/, (c)=>c.toUpperCase());
26
26
  }
27
- function CollapsibleText({ text, limit }) {
27
+ function CollapsibleText({ text, limit, translate }) {
28
28
  const [expanded, setExpanded] = useState(false);
29
29
  if (text.length <= limit) return /*#__PURE__*/ React.createElement("span", null, text);
30
30
  return /*#__PURE__*/ React.createElement("span", null, expanded ? text : `${text.slice(0, limit)}...`, /*#__PURE__*/ React.createElement("a", {
@@ -34,9 +34,9 @@ function CollapsibleText({ text, limit }) {
34
34
  fontSize: '0.85em',
35
35
  cursor: 'pointer'
36
36
  }
37
- }, expanded ? 'Show less' : 'Show more'));
37
+ }, expanded ? translate('agent.toolResult.showLess') : translate('agent.toolResult.showMore')));
38
38
  }
39
- function formatValue(value, depth) {
39
+ function formatValue(value, depth, translate) {
40
40
  if (value === null || value === undefined) {
41
41
  return /*#__PURE__*/ React.createElement("span", {
42
42
  style: {
@@ -67,26 +67,27 @@ function formatValue(value, depth) {
67
67
  if (value.length > 200) {
68
68
  return /*#__PURE__*/ React.createElement(CollapsibleText, {
69
69
  text: value,
70
- limit: 200
70
+ limit: 200,
71
+ translate: translate
71
72
  });
72
73
  }
73
74
  return /*#__PURE__*/ React.createElement("span", null, value);
74
75
  }
75
76
  if (Array.isArray(value)) {
76
- return formatArray(value, depth);
77
+ return formatArray(value, depth, translate);
77
78
  }
78
79
  if (typeof value === 'object') {
79
- return formatObject(value, depth);
80
+ return formatObject(value, depth, translate);
80
81
  }
81
82
  return /*#__PURE__*/ React.createElement("span", null, String(value));
82
83
  }
83
- function formatArray(arr, depth) {
84
+ function formatArray(arr, depth, translate) {
84
85
  if (arr.length === 0) {
85
86
  return /*#__PURE__*/ React.createElement("span", {
86
87
  style: {
87
88
  color: '#999'
88
89
  }
89
- }, "Empty list");
90
+ }, translate('agent.toolResult.emptyList'));
90
91
  }
91
92
  const allPrimitive = arr.every((item)=>item === null || item === undefined || typeof item !== 'object');
92
93
  if (allPrimitive) {
@@ -97,7 +98,7 @@ function formatArray(arr, depth) {
97
98
  }
98
99
  }, arr.map((item, i)=>/*#__PURE__*/ React.createElement("li", {
99
100
  key: i
100
- }, formatValue(item, depth + 1))));
101
+ }, formatValue(item, depth + 1, translate))));
101
102
  }
102
103
  return /*#__PURE__*/ React.createElement("div", {
103
104
  style: {
@@ -114,16 +115,16 @@ function formatArray(arr, depth) {
114
115
  background: 'rgba(0, 0, 0, 0.02)',
115
116
  border: '1px solid rgba(0, 0, 0, 0.06)'
116
117
  }
117
- }, formatValue(item, depth + 1))));
118
+ }, formatValue(item, depth + 1, translate))));
118
119
  }
119
- function formatObject(obj, depth) {
120
+ function formatObject(obj, depth, translate) {
120
121
  const keys = Object.keys(obj);
121
122
  if (keys.length === 0) {
122
123
  return /*#__PURE__*/ React.createElement("span", {
123
124
  style: {
124
125
  color: '#999'
125
126
  }
126
- }, "Empty");
127
+ }, translate('agent.toolResult.empty'));
127
128
  }
128
129
  return /*#__PURE__*/ React.createElement("div", {
129
130
  style: {
@@ -148,7 +149,7 @@ function formatObject(obj, depth) {
148
149
  fontSize: '0.9em',
149
150
  marginBottom: 2
150
151
  }
151
- }, humanizeKey(key)), formatValue(val, depth + 1));
152
+ }, humanizeKey(key)), formatValue(val, depth + 1, translate));
152
153
  }
153
154
  return /*#__PURE__*/ React.createElement("div", {
154
155
  key: key
@@ -158,16 +159,16 @@ function formatObject(obj, depth) {
158
159
  fontSize: '0.9em',
159
160
  color: 'rgba(0, 0, 0, 0.55)'
160
161
  }
161
- }, humanizeKey(key), ":", ' '), formatValue(val, depth + 1));
162
+ }, humanizeKey(key), ":", ' '), formatValue(val, depth + 1, translate));
162
163
  }));
163
164
  }
164
- function formatToolResult(output) {
165
+ function formatToolResult(output, translate) {
165
166
  if (output === null || output === undefined) {
166
167
  return /*#__PURE__*/ React.createElement("span", {
167
168
  style: {
168
169
  color: '#999'
169
170
  }
170
- }, "Completed (no data)");
171
+ }, translate('agent.toolResult.completedNoData'));
171
172
  }
172
173
  return /*#__PURE__*/ React.createElement("div", {
173
174
  style: {
@@ -176,6 +177,6 @@ function formatToolResult(output) {
176
177
  maxHeight: 400,
177
178
  overflowY: 'auto'
178
179
  }
179
- }, formatValue(output, 0));
180
+ }, formatValue(output, 0, translate));
180
181
  }
181
182
  export default formatToolResult;
@@ -21,6 +21,7 @@
21
21
  onToolCall: 'Trigger when a tool is invoked.',
22
22
  onToolResult: 'Trigger when a tool completes.',
23
23
  onUserMessage: 'Trigger when the user sends a message.',
24
+ onConversationStart: 'Trigger once when a conversation starts, on its first user message. Event contains the conversationId (auto-minted when no conversationId property is set).',
24
25
  onError: 'Trigger on stream error.',
25
26
  onFeedback: 'Trigger when the user clicks thumbs up or down on a message.',
26
27
  onRegenerate: 'Trigger when the user clicks regenerate on a message.',
@@ -13,12 +13,13 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { useRef, useEffect } from 'react';
16
- function useAgentEvents({ messages, status, methods, finishMetaRef }) {
16
+ function useAgentEvents({ messages, status, methods, finishMetaRef, conversationId }) {
17
17
  const prevStatusRef = useRef(status);
18
18
  const firedToolCallIds = useRef(new Set());
19
19
  const firedToolResultIds = useRef(new Set());
20
20
  const firedUserMessageIds = useRef(new Set());
21
21
  const firedTitleIds = useRef(new Set());
22
+ const firedConversationStartRef = useRef(false);
22
23
  const lastMessageCountRef = useRef(0);
23
24
  // Fire onMessageComplete when streaming finishes
24
25
  useEffect(()=>{
@@ -60,6 +61,18 @@ function useAgentEvents({ messages, status, methods, finishMetaRef }) {
60
61
  const lastMessage = messages[messages.length - 1];
61
62
  if (lastMessage && lastMessage.role === 'user' && !firedUserMessageIds.current.has(lastMessage.id)) {
62
63
  firedUserMessageIds.current.add(lastMessage.id);
64
+ // Announce the conversation once, on its first user message, with the
65
+ // effective (possibly auto-minted) conversationId so apps can persist or
66
+ // track with a guaranteed id.
67
+ if (!firedConversationStartRef.current) {
68
+ firedConversationStartRef.current = true;
69
+ methods.triggerEvent({
70
+ name: 'onConversationStart',
71
+ event: {
72
+ conversationId
73
+ }
74
+ });
75
+ }
63
76
  const textContent = lastMessage.parts?.filter((p)=>p.type === 'text').map((p)=>p.text).join('');
64
77
  methods.triggerEvent({
65
78
  name: 'onUserMessage',
@@ -78,7 +91,8 @@ function useAgentEvents({ messages, status, methods, finishMetaRef }) {
78
91
  }
79
92
  }, [
80
93
  messages,
81
- methods
94
+ methods,
95
+ conversationId
82
96
  ]);
83
97
  // Scan for new tool calls and results
84
98
  useEffect(()=>{
@@ -149,6 +163,7 @@ function useAgentEvents({ messages, status, methods, finishMetaRef }) {
149
163
  firedToolResultIds.current.clear();
150
164
  firedUserMessageIds.current.clear();
151
165
  firedTitleIds.current.clear();
166
+ firedConversationStartRef.current = false;
152
167
  }
153
168
  lastMessageCountRef.current = messages.length;
154
169
  }, [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/blocks-antd-x",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy Ant Design X Blocks",
6
6
  "homepage": "https://lowdefy.com",
@@ -41,8 +41,8 @@
41
41
  "@ant-design/icons": "6.1.0",
42
42
  "@ant-design/x": "2.7.0",
43
43
  "@ant-design/x-markdown": "2.7.0",
44
- "@lowdefy/block-utils": "5.3.0",
45
- "@lowdefy/helpers": "5.3.0",
44
+ "@lowdefy/block-utils": "5.4.0",
45
+ "@lowdefy/helpers": "5.4.0",
46
46
  "ai": "6.0.176"
47
47
  },
48
48
  "devDependencies": {