@lowdefy/blocks-antd-x 5.6.0 → 6.0.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.
@@ -19,6 +19,9 @@ import { FileCard, Prompts, Sender } from '@ant-design/x';
19
19
  import { Button } from 'antd';
20
20
  import { PaperClipOutlined } from '@ant-design/icons';
21
21
  import { isReserved, setKey, type } from '@lowdefy/helpers';
22
+ import getLegacyObjectUrl from '@lowdefy/blocks-files/utils/getLegacyObjectUrl.js';
23
+ import getUploadPolicy from '@lowdefy/blocks-files/utils/getUploadPolicy.js';
24
+ import uploadFile from '@lowdefy/blocks-files/utils/uploadFile.js';
22
25
  import { getFileCardType, getFileCardIcon } from './fileCardUtils.js';
23
26
  import DrawerWrapper from './DrawerWrapper.js';
24
27
  import createLowdefyChatTransport from './LowdefyChatTransport.js';
@@ -32,11 +35,16 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
32
35
  const fileInputRef = useRef(null);
33
36
  const [attachedFiles, setAttachedFiles] = useState([]);
34
37
  const [dragOver, setDragOver] = useState(false);
38
+ // Controlled composer value, so a starter prompt (or the setInput method) can
39
+ // fill the box, and a send can leave typed text in place when it is rejected.
40
+ const [inputValue, setInputValue] = useState('');
35
41
  // Mirror the operator-evaluated sharedState object into a ref so transport.body()
36
42
  // sees the freshest value at send time without re-constructing the transport.
37
43
  const sharedStateRef = useRef(null);
38
44
  sharedStateRef.current = type.isObject(sharedState) && Object.keys(sharedState).length > 0 ? sharedState : null;
39
45
  const attachmentsConfig = sender?.attachments;
46
+ const uploadPolicyRequestId = attachmentsConfig?.uploadPolicyRequestId ?? attachmentsConfig?.s3PostPolicyRequestId;
47
+ const downloadPolicyRequestId = attachmentsConfig?.downloadPolicyRequestId;
40
48
  const switchConfigs = sender?.switches ?? [];
41
49
  const [headerOpen, setHeaderOpen] = useState(sender?.header?.open ?? true);
42
50
  const [switchState, setSwitchState] = useState(()=>{
@@ -135,6 +143,13 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
135
143
  }
136
144
  },
137
145
  onError: (error)=>{
146
+ // A request that fails after the stream's `start` chunk leaves the
147
+ // assistant shell the SDK pushed on that chunk — id set, parts: [] — in
148
+ // the message list. Left there, every later send fails UIMessage
149
+ // validation ("Message must contain at least one part") and the
150
+ // conversation is dead until a reload. Drop empty shells so the user can
151
+ // simply try again; there is nothing in them to show or to send.
152
+ setMessages((prev)=>prev.filter((m)=>m.role !== 'assistant' || (m.parts?.length ?? 0) > 0));
138
153
  methods.triggerEvent({
139
154
  name: 'onError',
140
155
  event: {
@@ -236,7 +251,7 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
236
251
  sendMessage({
237
252
  text: args.text,
238
253
  ...args.files ? {
239
- experimental_attachments: args.files
254
+ files: args.files
240
255
  } : {},
241
256
  ...args.metadata ? {
242
257
  metadata: args.metadata
@@ -244,6 +259,9 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
244
259
  });
245
260
  }
246
261
  });
262
+ methods.registerMethod('setInput', (args)=>{
263
+ setInputValue(typeof args?.text === 'string' ? args.text : '');
264
+ });
247
265
  methods.registerMethod('clearMessages', ()=>{
248
266
  setMessages([]);
249
267
  });
@@ -275,19 +293,36 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
275
293
  }
276
294
  ]
277
295
  });
278
- if (attachmentsConfig?.s3PostPolicyRequestId) {
296
+ if (uploadPolicyRequestId) {
297
+ if (attachmentsConfig?.s3PostPolicyRequestId) {
298
+ console.warn('AgentChat attachments property "s3PostPolicyRequestId" is deprecated. Use "uploadPolicyRequestId" instead.');
299
+ }
279
300
  methods.registerEvent({
280
- name: '__getS3PostPolicy',
301
+ name: '__getUploadPolicy',
281
302
  actions: [
282
303
  {
283
- id: '__getS3PostPolicy',
304
+ id: '__getUploadPolicy',
284
305
  type: 'Request',
285
306
  params: [
286
- attachmentsConfig.s3PostPolicyRequestId
307
+ uploadPolicyRequestId
287
308
  ]
288
309
  }
289
310
  ]
290
311
  });
312
+ if (downloadPolicyRequestId) {
313
+ methods.registerEvent({
314
+ name: '__getDownloadPolicy',
315
+ actions: [
316
+ {
317
+ id: '__getDownloadPolicy',
318
+ type: 'Request',
319
+ params: [
320
+ downloadPolicyRequestId
321
+ ]
322
+ }
323
+ ]
324
+ });
325
+ }
291
326
  }
292
327
  }, []);
293
328
  useAgentEvents({
@@ -314,42 +349,41 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
314
349
  reader.readAsDataURL(file);
315
350
  });
316
351
  }
317
- async function uploadFileToS3(file) {
318
- const { name, size, type: fileType } = file;
319
- const s3PostPolicyResponse = await methods.triggerEvent({
320
- name: '__getS3PostPolicy',
321
- event: {
322
- file: {
323
- name,
324
- size,
325
- type: fileType
326
- }
327
- }
352
+ async function uploadFileToStorage(file) {
353
+ const { name, type: fileType } = file;
354
+ const descriptor = await getUploadPolicy({
355
+ methods,
356
+ file
328
357
  });
329
- if (s3PostPolicyResponse.success !== true) {
330
- throw new Error('S3 post policy request failed.');
331
- }
332
- const { url, fields = {} } = s3PostPolicyResponse.responses.__getS3PostPolicy.response[0];
333
- const { key } = fields;
334
- const formData = new FormData();
335
- Object.keys(fields).forEach((field)=>{
336
- formData.append(field, fields[field]);
358
+ await uploadFile({
359
+ descriptor,
360
+ file
337
361
  });
338
- formData.append('file', file);
339
- await new Promise((resolve, reject)=>{
340
- const xhr = new XMLHttpRequest();
341
- xhr.addEventListener('load', ()=>{
342
- if (xhr.status >= 200 && xhr.status < 300) {
343
- resolve();
344
- } else {
345
- reject(new Error(`S3 upload failed with status ${xhr.status}`));
362
+ // The model provider fetches this URL, so resolve it through the download
363
+ // request when configured (stable public URL or signed URL). Otherwise
364
+ // fall back to the legacy unsigned object URL from the descriptor.
365
+ let objectUrl;
366
+ if (downloadPolicyRequestId) {
367
+ const response = await methods.triggerEvent({
368
+ name: '__getDownloadPolicy',
369
+ event: {
370
+ file: {
371
+ bucket: descriptor.bucket,
372
+ key: descriptor.key,
373
+ name,
374
+ type: fileType
375
+ }
346
376
  }
347
377
  });
348
- xhr.addEventListener('error', ()=>reject(new Error('S3 upload network error')));
349
- xhr.open('POST', url);
350
- xhr.send(formData);
351
- });
352
- const objectUrl = url.endsWith('/') ? `${url}${key}` : `${url}/${key}`;
378
+ if (response.success !== true) {
379
+ throw new Error('Download policy request failed.');
380
+ }
381
+ objectUrl = response.responses.__getDownloadPolicy.response[0];
382
+ } else {
383
+ objectUrl = getLegacyObjectUrl({
384
+ descriptor
385
+ });
386
+ }
353
387
  return {
354
388
  url: objectUrl,
355
389
  mediaType: fileType,
@@ -417,9 +451,9 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
417
451
  text
418
452
  }
419
453
  ];
420
- if (attachmentsConfig?.s3PostPolicyRequestId) {
454
+ if (uploadPolicyRequestId) {
421
455
  try {
422
- const uploadResults = await Promise.all(attachedFiles.map((file)=>uploadFileToS3(file)));
456
+ const uploadResults = await Promise.all(attachedFiles.map((file)=>uploadFileToStorage(file)));
423
457
  for (const result of uploadResults){
424
458
  parts.push({
425
459
  type: 'file',
@@ -432,7 +466,7 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
432
466
  methods.triggerEvent({
433
467
  name: 'onError',
434
468
  event: {
435
- message: error.message ?? 'File upload to S3 failed.'
469
+ message: error.message ?? 'File upload failed.'
436
470
  }
437
471
  });
438
472
  return;
@@ -452,7 +486,12 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
452
486
  text
453
487
  });
454
488
  }
455
- senderRef.current?.clear();
489
+ // Empty the composer here, after the sends and downstream of both the
490
+ // onBeforeSend cancellation return and the upload await, so a send that was
491
+ // rejected or failed to upload leaves the user's text in the box. Resetting
492
+ // from the Sender's onSubmit handler instead would silently lose typed input
493
+ // on every rejected send.
494
+ setInputValue('');
456
495
  }
457
496
  function handleStop() {
458
497
  stop();
@@ -468,6 +507,12 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
468
507
  text: prompt.label
469
508
  });
470
509
  }
510
+ // A two-track welcome starter fills the composer instead of sending, so a
511
+ // generic shipped default is an editable first draft rather than a message the
512
+ // user never meant to send. Reuses the controlled Sender value.
513
+ function handleWelcomePromptFill(text) {
514
+ setInputValue(typeof text === 'string' ? text : '');
515
+ }
471
516
  function handleSuggestionClick(suggestion) {
472
517
  methods.triggerEvent({
473
518
  name: 'onSuggestionClick',
@@ -609,7 +654,7 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
609
654
  minHeight: 0,
610
655
  padding: '16px 0'
611
656
  }
612
- }, isEmpty ? /*#__PURE__*/ React.createElement(WelcomeScreen, {
657
+ }, isEmpty && !welcome?.tracks ? /*#__PURE__*/ React.createElement(WelcomeScreen, {
613
658
  config: welcome,
614
659
  onPromptClick: handlePromptClick
615
660
  }) : /*#__PURE__*/ React.createElement(MessageList, {
@@ -617,6 +662,8 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
617
662
  messages: messages,
618
663
  isStreaming: isBusy,
619
664
  config: messageDisplay,
665
+ welcome: welcome,
666
+ onWelcomePromptFill: handleWelcomePromptFill,
620
667
  addToolApprovalResponse: addToolApprovalResponse,
621
668
  onFeedback: handleFeedback,
622
669
  onLinkClick: handleLinkClick,
@@ -699,6 +746,8 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
699
746
  }
700
747
  }), /*#__PURE__*/ React.createElement(Sender, {
701
748
  ref: senderRef,
749
+ value: inputValue,
750
+ onChange: setInputValue,
702
751
  placeholder: sender?.placeholder ?? methods.translate('agent.sender.placeholder'),
703
752
  submitType: sender?.submitType ?? 'enter',
704
753
  allowSpeech: sender?.allowSpeech ?? false,
@@ -741,7 +790,6 @@ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageI
741
790
  }
742
791
  AgentChat.meta = {
743
792
  category: 'display',
744
- icons: [],
745
- styles: []
793
+ icons: []
746
794
  };
747
795
  export default AgentChat;
@@ -14,7 +14,9 @@
14
14
  limitations under the License.
15
15
  */ import { DefaultChatTransport } from 'ai';
16
16
  function createLowdefyChatTransport({ pageId, agentId, conversationId, urlQuery, sharedStateRef }) {
17
- const base = `/api/agent/${pageId}/${agentId}`;
17
+ // The server route takes the last path segment as the agentId — module-scoped
18
+ // agent ids contain '/', so encode the agentId into a single segment.
19
+ const base = `/api/agent/${pageId}/${encodeURIComponent(agentId)}`;
18
20
  const api = conversationId ? `${base}?conversationId=${encodeURIComponent(conversationId)}` : base;
19
21
  return new DefaultChatTransport({
20
22
  api,
@@ -21,6 +21,7 @@ import { hasVisibleContent } from './messageParts.js';
21
21
  import MessageBubble from './MessageBubble.js';
22
22
  import ThinkingIndicator from './ThinkingIndicator.js';
23
23
  import useThinkingMessage from './useThinkingMessage.js';
24
+ import WelcomeScreen from './WelcomeScreen.js';
24
25
  function roleAvatar(roleConfig, fallbackIcon) {
25
26
  if (roleConfig?.avatar) {
26
27
  return /*#__PURE__*/ React.createElement(Avatar, {
@@ -55,7 +56,7 @@ function ThinkingBubbleContent({ bubbleId, config }) {
55
56
  label: label
56
57
  });
57
58
  }
58
- const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messages, isStreaming, config, addToolApprovalResponse, onFeedback, onLinkClick, Link, feedbackValues, onRegenerate, onDelete, onEditMessage, translate }, ref) {
59
+ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messages, isStreaming, config, welcome, onWelcomePromptFill, addToolApprovalResponse, onFeedback, onLinkClick, Link, feedbackValues, onRegenerate, onDelete, onEditMessage, translate }, ref) {
59
60
  // Build a lookup map for message parts.
60
61
  // Bubble.List's contentRender callback only receives (content, info) where info.key is the
61
62
  // item key — it does not receive the full message object with its parts array. This map
@@ -119,10 +120,36 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
119
120
  streaming: true
120
121
  });
121
122
  }
123
+ // The welcome renders as a leading item so it scrolls up with the transcript
124
+ // and stays reachable by scrolling back, rather than being swapped out on the
125
+ // first send. With no messages yet it is simply the only item in the list.
126
+ if (welcome?.tracks) {
127
+ items.unshift({
128
+ key: '__welcome__',
129
+ role: 'welcome'
130
+ });
131
+ }
122
132
  return /*#__PURE__*/ React.createElement(Bubble.List, {
123
133
  ref: ref,
124
134
  items: items,
125
135
  role: {
136
+ welcome: {
137
+ placement: 'start',
138
+ variant: 'borderless',
139
+ style: {
140
+ maxWidth: '100%'
141
+ },
142
+ styles: {
143
+ content: {
144
+ width: '100%'
145
+ }
146
+ },
147
+ contentRender: ()=>/*#__PURE__*/ React.createElement(WelcomeScreen, {
148
+ config: welcome,
149
+ onFill: onWelcomePromptFill,
150
+ inFlow: true
151
+ })
152
+ },
126
153
  user: {
127
154
  placement: 'end',
128
155
  variant: config?.roles?.user?.variant ?? 'filled',
@@ -14,8 +14,95 @@
14
14
  limitations under the License.
15
15
  */ import React from 'react';
16
16
  import { Welcome, Prompts } from '@ant-design/x';
17
- function WelcomeScreen({ config, onPromptClick }) {
17
+ import { Button } from 'antd';
18
+ // One track: a bordered card holding a heading and a column of starter chips.
19
+ // A chip fills the composer (onFill) rather than sending — a near-miss starter
20
+ // becomes an editable first draft, not a message the user never meant to send.
21
+ function TrackCard({ track, onFill }) {
22
+ const prompts = track.prompts ?? [];
23
+ return /*#__PURE__*/ React.createElement("div", {
24
+ style: {
25
+ flex: '1 1 240px',
26
+ border: '1px solid var(--ant-color-border)',
27
+ borderRadius: 'var(--ant-border-radius-lg)',
28
+ padding: 16,
29
+ backgroundColor: 'var(--ant-color-bg-container)',
30
+ display: 'flex',
31
+ flexDirection: 'column',
32
+ gap: 8
33
+ }
34
+ }, track.label && /*#__PURE__*/ React.createElement("h5", {
35
+ style: {
36
+ margin: '0 0 4px',
37
+ fontSize: 'var(--ant-font-size-heading-5)',
38
+ fontWeight: 'var(--ant-font-weight-strong, 600)',
39
+ lineHeight: 'var(--ant-line-height-heading-5)'
40
+ }
41
+ }, track.label), prompts.map((prompt, index)=>{
42
+ const text = typeof prompt === 'string' ? prompt : prompt.label;
43
+ return /*#__PURE__*/ React.createElement(Button, {
44
+ key: index,
45
+ block: true,
46
+ // antd buttons are nowrap and size to their longest line; height auto
47
+ // plus whiteSpace normal lets a sentence-length starter wrap left.
48
+ style: {
49
+ height: 'auto',
50
+ whiteSpace: 'normal',
51
+ textAlign: 'left',
52
+ paddingTop: 6,
53
+ paddingBottom: 6
54
+ },
55
+ onClick: ()=>onFill?.(text)
56
+ }, text);
57
+ }));
58
+ }
59
+ // The chat empty state. Two modes:
60
+ // - `config.tracks` present → a two-track teaching panel (ask a question /
61
+ // build a report), each track a card of fill-the-composer starters. When
62
+ // `inFlow` it renders top-aligned as a leading item inside the message
63
+ // scroll area, so it scrolls up with the conversation and stays reachable
64
+ // by scrolling back rather than being swapped out on the first send.
65
+ // - otherwise → the flat Welcome + Prompts row, centred, sending on click.
66
+ function WelcomeScreen({ config, onPromptClick, onFill, inFlow }) {
18
67
  if (!config) return null;
68
+ if (config.tracks) {
69
+ return /*#__PURE__*/ React.createElement("div", {
70
+ style: inFlow ? {
71
+ display: 'flex',
72
+ flexDirection: 'column',
73
+ gap: 16,
74
+ width: '100%'
75
+ } : {
76
+ flex: 1,
77
+ display: 'flex',
78
+ flexDirection: 'column',
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ gap: 16
82
+ }
83
+ }, config.title && /*#__PURE__*/ React.createElement("h3", {
84
+ style: {
85
+ margin: 0,
86
+ fontSize: 'var(--ant-font-size-heading-3)',
87
+ fontWeight: 'var(--ant-font-weight-strong, 600)',
88
+ lineHeight: 'var(--ant-line-height-heading-3)'
89
+ }
90
+ }, config.title), config.description && /*#__PURE__*/ React.createElement("div", {
91
+ style: {
92
+ color: 'var(--ant-color-text-secondary)'
93
+ }
94
+ }, config.description), /*#__PURE__*/ React.createElement("div", {
95
+ style: {
96
+ display: 'flex',
97
+ gap: 12,
98
+ flexWrap: 'wrap'
99
+ }
100
+ }, config.tracks.map((track, index)=>/*#__PURE__*/ React.createElement(TrackCard, {
101
+ key: index,
102
+ track: track,
103
+ onFill: onFill
104
+ }))));
105
+ }
19
106
  const promptItems = (config.prompts ?? []).map((prompt, index)=>({
20
107
  key: prompt.key ?? `prompt-${index}`,
21
108
  label: prompt.label,
@@ -201,9 +201,17 @@
201
201
  "type": "number",
202
202
  "description": "Maximum file size in bytes. Larger files are silently skipped."
203
203
  },
204
+ "uploadPolicyRequestId": {
205
+ "type": "string",
206
+ "description": "Id of an upload-policy request (e.g. AwsS3PresignedPostPolicy, GcsSignedPostPolicy, AzureBlobUploadSas). When set, files are uploaded to storage and their URLs are sent to the LLM instead of base64 data."
207
+ },
208
+ "downloadPolicyRequestId": {
209
+ "type": "string",
210
+ "description": "Id of a download request used to resolve the URL of an uploaded attachment. The URL is fetched by the model provider, so it should remain reachable — use public: true on the request for public buckets, or a long expiry. When unset, a legacy unsigned object URL is constructed from the upload response (S3-shaped, deprecated)."
211
+ },
204
212
  "s3PostPolicyRequestId": {
205
213
  "type": "string",
206
- "description": "Request ID of type AwsS3PresignedPostPolicy. When set, files are uploaded to S3 and their URLs are sent to the LLM instead of base64 data."
214
+ "description": "Deprecated — use uploadPolicyRequestId instead."
207
215
  }
208
216
  }
209
217
  },
@@ -0,0 +1,121 @@
1
+ # Copyright 2020-2026 Lowdefy, Inc
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ id: agent-chat
16
+ type: PageHeaderMenu
17
+ properties:
18
+ title: AgentChat
19
+ blocks:
20
+ # Both chats are driven from the messages property, so nothing here calls the agent.
21
+ - id: chat_wired
22
+ type: AgentChat
23
+ properties:
24
+ agentId: test_agent
25
+ height: 300
26
+ conversationId: wired
27
+ messageDisplay:
28
+ actions:
29
+ - feedback
30
+ messages:
31
+ - id: w1
32
+ role: assistant
33
+ parts:
34
+ - type: text
35
+ text: |
36
+ [in-app link](/agent-chat-target) and [external link](https://example.com)
37
+ and [**bold** link](/agent-chat-target?tab=two)
38
+ events:
39
+ onFeedback:
40
+ - id: record_rating
41
+ type: SetState
42
+ params:
43
+ last_rating:
44
+ _event: rating
45
+ onLinkClick:
46
+ - id: record_link_click
47
+ type: SetState
48
+ params:
49
+ last_href:
50
+ _event: href
51
+ last_text:
52
+ _event: text
53
+ click_count:
54
+ _sum:
55
+ - _if_none:
56
+ - _state: click_count
57
+ - 0
58
+ - 1
59
+
60
+ # Seeded from feedbackValues rather than from a click: what a reload or a conversation
61
+ # switch looks like to an app that stored the rating itself.
62
+ - id: chat_seeded
63
+ type: AgentChat
64
+ properties:
65
+ agentId: test_agent
66
+ height: 300
67
+ conversationId: seeded
68
+ messageDisplay:
69
+ actions:
70
+ - feedback
71
+ feedbackValues:
72
+ s1: like
73
+ messages:
74
+ - id: s1
75
+ role: assistant
76
+ parts:
77
+ - type: text
78
+ text: A rated answer.
79
+ events:
80
+ onFeedback:
81
+ - id: record_seeded_rating
82
+ type: SetState
83
+ params:
84
+ seeded_rating:
85
+ _event: rating
86
+
87
+ - id: wired_readout
88
+ type: Html
89
+ properties:
90
+ html:
91
+ _nunjucks:
92
+ template: |
93
+ <span id="readout_href">{{ href }}</span>
94
+ <span id="readout_text">{{ text }}</span>
95
+ <span id="readout_count">{{ count or 0 }}</span>
96
+ <span id="readout_rating">{{ rating }}</span>
97
+ <span id="readout_seeded_rating">{{ seeded_rating }}</span>
98
+ on:
99
+ href:
100
+ _state: last_href
101
+ text:
102
+ _state: last_text
103
+ count:
104
+ _state: click_count
105
+ rating:
106
+ _state: last_rating
107
+ seeded_rating:
108
+ _state: seeded_rating
109
+
110
+ - id: chat_unwired
111
+ type: AgentChat
112
+ properties:
113
+ agentId: test_agent
114
+ height: 300
115
+ conversationId: unwired
116
+ messages:
117
+ - id: u1
118
+ role: assistant
119
+ parts:
120
+ - type: text
121
+ text: '[in-app link](/agent-chat-target) and [external link](https://example.com)'
@@ -0,0 +1,25 @@
1
+ # Copyright 2020-2026 Lowdefy, Inc
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ id: agent-chat-target
16
+ type: PageHeaderMenu
17
+ properties:
18
+ title: Link target
19
+ blocks:
20
+ - id: target_marker
21
+ type: Html
22
+ properties:
23
+ # No id on the span: Lowdefy renders the block id into the DOM, so repeating it here
24
+ # gives two matching elements and a strict-mode violation.
25
+ html: <span>arrived</span>
@@ -82,9 +82,6 @@ function AgentConversations({ blockId, methods, properties }) {
82
82
  }
83
83
  AgentConversations.meta = {
84
84
  category: 'display',
85
- icons: [],
86
- styles: [
87
- 'blocks/AgentConversations/style.less'
88
- ]
85
+ icons: []
89
86
  };
90
87
  export default AgentConversations;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/blocks-antd-x",
3
- "version": "5.6.0",
3
+ "version": "6.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy Ant Design X Blocks",
6
6
  "homepage": "https://lowdefy.com",
@@ -41,16 +41,17 @@
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.6.0",
45
- "@lowdefy/helpers": "5.6.0",
44
+ "@lowdefy/block-utils": "6.0.0",
45
+ "@lowdefy/blocks-files": "6.0.0",
46
+ "@lowdefy/helpers": "6.0.0",
46
47
  "ai": "6.0.176"
47
48
  },
48
49
  "devDependencies": {
49
- "@lowdefy/block-dev-e2e": "5.6.0",
50
- "@lowdefy/e2e-utils": "5.6.0",
51
- "@playwright/test": "1.50.1",
52
- "@swc/cli": "0.8.0",
53
- "@swc/core": "1.15.18",
50
+ "@lowdefy/block-dev-e2e": "6.0.0",
51
+ "@lowdefy/e2e-utils": "6.0.0",
52
+ "@playwright/test": "1.59.1",
53
+ "@swc/cli": "0.8.1",
54
+ "@swc/core": "1.15.32",
54
55
  "copyfiles": "2.4.1"
55
56
  },
56
57
  "peerDependencies": {
@@ -63,7 +64,7 @@
63
64
  "scripts": {
64
65
  "build": "swc src --out-dir dist --config-file ../../../../.swcrc --cli-config-file ../../../../.swc-cli.json && pnpm copyfiles",
65
66
  "clean": "rm -rf dist",
66
- "copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.yaml\" -e \"./src/**/*.snap\"",
67
+ "copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.snap\"",
67
68
  "e2e": "playwright test --config e2e/playwright.config.js",
68
69
  "e2e:ui": "playwright test --config e2e/playwright.config.js --ui"
69
70
  }
@@ -1 +0,0 @@
1
- // AgentConversations styles — currently uses inline styles and Ant Design X defaults.