@lowdefy/blocks-antd-x 5.5.0 → 5.6.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.
@@ -12,25 +12,26 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import React, { useRef, useMemo, useEffect, useState } from 'react';
15
+ */ import React, { useRef, useMemo, useEffect, useState, useCallback } from 'react';
16
16
  import { useChat } from '@ai-sdk/react';
17
17
  import { lastAssistantMessageIsCompleteWithApprovalResponses, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
18
18
  import { FileCard, Prompts, Sender } from '@ant-design/x';
19
19
  import { Button } from 'antd';
20
20
  import { PaperClipOutlined } from '@ant-design/icons';
21
- import { type } from '@lowdefy/helpers';
21
+ import { isReserved, setKey, type } from '@lowdefy/helpers';
22
22
  import { getFileCardType, getFileCardIcon } from './fileCardUtils.js';
23
23
  import DrawerWrapper from './DrawerWrapper.js';
24
24
  import createLowdefyChatTransport from './LowdefyChatTransport.js';
25
25
  import MessageList from './MessageList.js';
26
26
  import useAgentEvents, { collectExternalEventIds } from './useAgentEvents.js';
27
27
  import WelcomeScreen from './WelcomeScreen.js';
28
- function AgentChat({ blockId, components: { Icon }, methods, pageId, properties }) {
29
- const { agentId, urlQuery, sharedState, welcome, messageDisplay, sender, conversationId, messages: externalMessages, display, drawer: drawerConfig, suggestions } = properties;
28
+ function AgentChat({ blockId, components: { Icon, Link }, events, methods, pageId, properties }) {
29
+ const { agentId, urlQuery, sharedState, welcome, messageDisplay, sender, conversationId, feedbackValues: storedFeedbackValues, messages: externalMessages, display, drawer: drawerConfig, suggestions } = properties;
30
30
  const senderRef = useRef(null);
31
31
  const finishMetaRef = useRef(null);
32
32
  const fileInputRef = useRef(null);
33
33
  const [attachedFiles, setAttachedFiles] = useState([]);
34
+ const [dragOver, setDragOver] = useState(false);
34
35
  // Mirror the operator-evaluated sharedState object into a ref so transport.body()
35
36
  // sees the freshest value at send time without re-constructing the transport.
36
37
  const sharedStateRef = useRef(null);
@@ -68,6 +69,16 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
68
69
  ]);
69
70
  const bubbleListRef = useRef(null);
70
71
  const { messages, sendMessage, status, stop, addToolApprovalResponse, addToolOutput, setMessages, regenerate, clearError } = useChat({
72
+ // Key the Chat instance by conversation: without an id, useChat creates its
73
+ // Chat once per mount and CAPTURES that transport — the transport rebuilt by
74
+ // the useMemo above on a conversationId change is silently ignored, so every
75
+ // send posts under the mount-time conversationId. Continuing a conversation
76
+ // the app selected later then persists the whole transcript under the stale
77
+ // id (a duplicate conversation doc). Keying by id makes useChat swap Chat
78
+ // instances — and adopt the rebuilt transport — when the conversation
79
+ // changes; the clear-on-id-change and external-message-sync effects below
80
+ // compose with the swap unchanged.
81
+ id: effectiveConversationId,
71
82
  transport,
72
83
  experimental_throttle: 50,
73
84
  sendAutomaticallyWhen: (args)=>lastAssistantMessageIsCompleteWithToolCalls(args) || lastAssistantMessageIsCompleteWithApprovalResponses(args),
@@ -84,8 +95,17 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
84
95
  const writable = {};
85
96
  const ignored = [];
86
97
  for (const [key, value] of Object.entries(updates)){
87
- if (allowedKeys.has(key)) writable[key] = value;
88
- else ignored.push(key);
98
+ if (!allowedKeys.has(key)) {
99
+ ignored.push(key);
100
+ continue;
101
+ }
102
+ // Same policy as a non-allowlisted key: drop it and report it back
103
+ // so the model can self-correct on the next turn.
104
+ if (isReserved(key)) {
105
+ ignored.push(key);
106
+ continue;
107
+ }
108
+ setKey(writable, key, value);
89
109
  }
90
110
  if (Object.keys(writable).length > 0) {
91
111
  await methods.triggerEvent({
@@ -140,6 +160,21 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
140
160
  });
141
161
  }
142
162
  });
163
+ // The control is otherwise write-only: it reports a rating and immediately forgets it,
164
+ // so the thumb un-highlights on the next render and the message looks unrated. This holds
165
+ // what was clicked THIS visit; the block still persists nothing itself.
166
+ const [feedbackValues, setFeedbackValues] = useState({});
167
+ // Ratings the app already knows about, keyed by message id — what a reload or a
168
+ // conversation switch would otherwise lose, since the block stores nothing. A click this
169
+ // visit wins over the stored value, so the thumb responds immediately and a rating the
170
+ // user has just withdrawn is not re-lit by a stale prop.
171
+ // Not memoised: the property arrives from operators that rebuild it every render, so a
172
+ // dependency on it would miss every time — the same reason the message sync below
173
+ // compares by count and id rather than by reference.
174
+ const effectiveFeedbackValues = {
175
+ ...storedFeedbackValues ?? {},
176
+ ...feedbackValues
177
+ };
143
178
  // Clear messages when conversationId changes so the new conversation starts clean.
144
179
  // Developers load saved messages via the messages property if needed.
145
180
  const prevConversationIdRef = useRef(effectiveConversationId);
@@ -147,6 +182,9 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
147
182
  if (effectiveConversationId !== prevConversationIdRef.current) {
148
183
  prevConversationIdRef.current = effectiveConversationId;
149
184
  setMessages([]);
185
+ // Ratings clicked in the thread being left must not carry over: they are keyed by
186
+ // message id, and the incoming conversation supplies its own through feedbackValues.
187
+ setFeedbackValues({});
150
188
  }
151
189
  }, [
152
190
  effectiveConversationId,
@@ -318,6 +356,42 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
318
356
  filename: name
319
357
  };
320
358
  }
359
+ // One intake for every way a file arrives — the paperclip picker, a
360
+ // clipboard paste, a drag-and-drop — so the accept list and size cap apply
361
+ // to all of them, not just the picker's native dialog. Pasted clipboard
362
+ // images all arrive as "image.png" (every browser), and the attached list
363
+ // is keyed by name + size, so those get a unique name.
364
+ function acceptsFile(file) {
365
+ const accept = attachmentsConfig?.accept;
366
+ if (!accept) return true;
367
+ const name = (file.name || '').toLowerCase();
368
+ const mime = (file.type || '').toLowerCase();
369
+ return accept.split(',').map((t)=>t.trim().toLowerCase()).filter(Boolean).some((t)=>{
370
+ if (t.startsWith('.')) return name.endsWith(t);
371
+ if (t.endsWith('/*')) return mime.startsWith(t.slice(0, -1));
372
+ return mime === t;
373
+ });
374
+ }
375
+ function addFiles(incoming, { pasted = false } = {}) {
376
+ const files = Array.from(incoming ?? []).filter((f)=>acceptsFile(f) && !(attachmentsConfig?.maxSize && f.size > attachmentsConfig.maxSize));
377
+ if (files.length === 0) return;
378
+ const named = files.map((f)=>{
379
+ if (!pasted || !/^image\.\w+$/i.test(f.name)) return f;
380
+ const ext = f.name.split('.').pop();
381
+ return new File([
382
+ f
383
+ ], `pasted-${Date.now()}.${ext}`, {
384
+ type: f.type
385
+ });
386
+ });
387
+ setAttachedFiles((prev)=>[
388
+ ...prev,
389
+ ...named
390
+ ]);
391
+ }
392
+ function dragHasFiles(e) {
393
+ return Array.from(e.dataTransfer?.types ?? []).includes('Files');
394
+ }
321
395
  async function handleSend(text) {
322
396
  if (!text.trim() && attachedFiles.length === 0) return;
323
397
  const filesMeta = attachedFiles.map((file)=>({
@@ -431,7 +505,40 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
431
505
  messages
432
506
  ]);
433
507
  const activeSuggestions = agentSuggestions ?? suggestions;
508
+ // A link in an answer was a plain anchor: a full browser navigation out of the
509
+ // conversation, with no way for an app to do anything else with it. Default is only
510
+ // prevented when the app actually handles onLinkClick — otherwise the link keeps
511
+ // navigating, so wiring nothing changes nothing.
512
+ //
513
+ // Modified and non-primary clicks are never intercepted: open-in-new-tab is a
514
+ // reasonable thing to do with a citation, and preventing it would be a regression.
515
+ const interceptLinks = Boolean(events?.onLinkClick);
516
+ // Stable identity: MessageBubble memoises its markdown component map on this, and the map
517
+ // is rebuilt on every streaming chunk otherwise — remounting every link in the answer as
518
+ // it streams.
519
+ const handleLinkClick = useCallback(({ href, text, domEvent })=>{
520
+ if (!interceptLinks || domEvent.defaultPrevented || domEvent.button !== 0 || domEvent.metaKey || domEvent.ctrlKey || domEvent.shiftKey || domEvent.altKey) {
521
+ return;
522
+ }
523
+ // Prevented synchronously: Link checks defaultPrevented immediately after calling
524
+ // this, and an external anchor's default fires before any async work could run.
525
+ domEvent.preventDefault();
526
+ methods.triggerEvent({
527
+ name: 'onLinkClick',
528
+ event: {
529
+ href,
530
+ text
531
+ }
532
+ });
533
+ }, [
534
+ interceptLinks,
535
+ methods
536
+ ]);
434
537
  function handleFeedback({ messageId, rating }) {
538
+ setFeedbackValues((prev)=>({
539
+ ...prev,
540
+ [messageId]: rating
541
+ }));
435
542
  const message = messages.find((msg)=>msg.id === messageId);
436
543
  const messageContent = message?.parts?.filter((part)=>part.type === 'text').map((part)=>part.text).join('') ?? '';
437
544
  methods.triggerEvent({
@@ -512,6 +619,9 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
512
619
  config: messageDisplay,
513
620
  addToolApprovalResponse: addToolApprovalResponse,
514
621
  onFeedback: handleFeedback,
622
+ onLinkClick: handleLinkClick,
623
+ Link: Link,
624
+ feedbackValues: effectiveFeedbackValues,
515
625
  onRegenerate: handleRegenerate,
516
626
  onDelete: handleDelete,
517
627
  onEditMessage: handleEditMessage,
@@ -530,8 +640,27 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
530
640
  wrap: true
531
641
  })), /*#__PURE__*/ React.createElement("div", {
532
642
  style: {
533
- padding: '8px 16px 24px'
534
- }
643
+ padding: '8px 16px 24px',
644
+ borderRadius: 12,
645
+ outline: dragOver ? '2px dashed currentColor' : 'none',
646
+ outlineOffset: -6,
647
+ transition: 'outline-color 120ms'
648
+ },
649
+ // Drag-in lands in the same intake as the picker and paste. Only
650
+ // react to drags carrying files, so text selections dragged across
651
+ // the composer are left to the textarea.
652
+ onDragOver: attachmentsConfig?.enabled ? (e)=>{
653
+ if (!dragHasFiles(e)) return;
654
+ e.preventDefault();
655
+ if (!dragOver) setDragOver(true);
656
+ } : undefined,
657
+ onDragLeave: attachmentsConfig?.enabled ? ()=>setDragOver(false) : undefined,
658
+ onDrop: attachmentsConfig?.enabled ? (e)=>{
659
+ if (!dragHasFiles(e)) return;
660
+ e.preventDefault();
661
+ setDragOver(false);
662
+ addFiles(e.dataTransfer.files);
663
+ } : undefined
535
664
  }, attachmentsConfig?.enabled && attachedFiles.length > 0 && /*#__PURE__*/ React.createElement(FileCard.List, {
536
665
  style: {
537
666
  marginBottom: 4
@@ -565,12 +694,7 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
565
694
  accept: attachmentsConfig.accept,
566
695
  multiple: true,
567
696
  onChange: (e)=>{
568
- const files = Array.from(e.target.files);
569
- const valid = files.filter((f)=>!(attachmentsConfig.maxSize && f.size > attachmentsConfig.maxSize));
570
- setAttachedFiles((prev)=>[
571
- ...prev,
572
- ...valid
573
- ]);
697
+ addFiles(e.target.files);
574
698
  e.target.value = '';
575
699
  }
576
700
  }), /*#__PURE__*/ React.createElement(Sender, {
@@ -579,6 +703,11 @@ function AgentChat({ blockId, components: { Icon }, methods, pageId, properties
579
703
  submitType: sender?.submitType ?? 'enter',
580
704
  allowSpeech: sender?.allowSpeech ?? false,
581
705
  onSubmit: handleSend,
706
+ // Clipboard paste (a screenshot, a copied file) attaches like the
707
+ // paperclip does.
708
+ onPasteFile: attachmentsConfig?.enabled ? (files)=>addFiles(files, {
709
+ pasted: true
710
+ }) : undefined,
582
711
  onCancel: handleStop,
583
712
  loading: isBusy,
584
713
  prefix: attachmentsConfig?.enabled ? /*#__PURE__*/ React.createElement(Button, {
@@ -101,6 +101,52 @@ function resolveToolResultMode(toolResultDisplay, toolName) {
101
101
  }
102
102
  return 'summary';
103
103
  }
104
+ // Plain link text arrives as a string, but any inline formatting makes it an element tree —
105
+ // so it is flattened rather than reported as absent.
106
+ function linkText(children) {
107
+ if (typeof children === 'string') return children;
108
+ if (typeof children === 'number') return String(children);
109
+ if (Array.isArray(children)) return children.map(linkText).join('');
110
+ if (/*#__PURE__*/ React.isValidElement(children)) return linkText(children.props?.children);
111
+ return '';
112
+ }
113
+ // An in-app path renders through the app's Link, so following a citation is a client-side
114
+ // route rather than a reload that drops the conversation. Anything with a scheme opens in a
115
+ // new tab for the same reason. Both still report the click, so an app that handles
116
+ // onLinkClick can show the target in place instead.
117
+ function MarkdownLink({ Link, onLinkClick }) {
118
+ return function MarkdownAnchor({ href, children, ...props }) {
119
+ if (!href) return /*#__PURE__*/ React.createElement("span", props, children);
120
+ const text = linkText(children);
121
+ const handleClick = (domEvent)=>onLinkClick?.({
122
+ href,
123
+ text,
124
+ domEvent
125
+ });
126
+ const external = /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//');
127
+ // A fragment cannot be expressed as a pageId link, so it keeps the anchor.
128
+ const inApp = !external && href.startsWith('/') && !href.includes('#');
129
+ if (!inApp || !Link) {
130
+ return /*#__PURE__*/ React.createElement("a", {
131
+ ...props,
132
+ href: href,
133
+ target: external ? '_blank' : undefined,
134
+ rel: external ? 'noopener noreferrer' : undefined,
135
+ onClick: handleClick
136
+ }, children);
137
+ }
138
+ // `pageId`, not `href`: Link renders a plain anchor for href and url — its new-origin form —
139
+ // and only routes client-side for a pageId. Passing the path as href therefore went through
140
+ // Link and still reloaded the page, dropping the conversation this exists to preserve.
141
+ const [pathname, search = ''] = href.slice(1).split('?');
142
+ return /*#__PURE__*/ React.createElement(Link, {
143
+ ...props,
144
+ pageId: pathname,
145
+ urlQuery: Object.fromEntries(new URLSearchParams(search)),
146
+ onClick: handleClick
147
+ }, children);
148
+ };
149
+ }
104
150
  function normalizeActions(actions) {
105
151
  if (Array.isArray(actions)) {
106
152
  const obj = {};
@@ -111,7 +157,7 @@ function normalizeActions(actions) {
111
157
  }
112
158
  return actions ?? {};
113
159
  }
114
- function BubbleActions({ actions, textContent, messageId, onFeedback, onRegenerate, onDelete, translate }) {
160
+ function BubbleActions({ actions, textContent, messageId, onFeedback, feedbackValue, onRegenerate, onDelete, translate }) {
115
161
  const normalized = normalizeActions(actions);
116
162
  const items = [];
117
163
  if (normalized.copy) {
@@ -128,6 +174,7 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, onRegenera
128
174
  key: 'feedback',
129
175
  label: translate('agent.message.feedback'),
130
176
  actionRender: ()=>/*#__PURE__*/ React.createElement(Actions.Feedback, {
177
+ value: feedbackValue ?? 'default',
131
178
  onChange: (rating)=>onFeedback?.({
132
179
  messageId,
133
180
  rating
@@ -176,7 +223,7 @@ function SourcesDisplay({ sourceParts, config, translate }) {
176
223
  expandIconPosition: config?.sourcesDisplay?.expandIconPosition ?? 'end'
177
224
  });
178
225
  }
179
- function MessageBubble({ content, isStreaming, parts, config, addToolApprovalResponse, actions, messageId, onFeedback, onRegenerate, onDelete, translate }) {
226
+ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalResponse, actions, messageId, onFeedback, feedbackValue, onLinkClick, Link, onRegenerate, onDelete, translate }) {
180
227
  const showThoughtChain = config?.showThoughtChain !== false;
181
228
  const showReasoning = config?.showReasoning !== false;
182
229
  const reasoningDisplay = config?.reasoningDisplay ?? 'interleaved';
@@ -186,18 +233,22 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
186
233
  const renderLatex = config?.renderLatex ?? false;
187
234
  const markdownConfig = renderLatex ? getLatexConfig() : undefined;
188
235
  const markdownComponents = useMemo(()=>{
189
- if (!renderMermaid && !codeHighlighter) return {
190
- code: PlainCodeBlock
191
- };
236
+ const code = !renderMermaid && !codeHighlighter ? PlainCodeBlock : RichCodeBlock({
237
+ renderMermaid,
238
+ codeHighlighter
239
+ });
192
240
  return {
193
- code: RichCodeBlock({
194
- renderMermaid,
195
- codeHighlighter
241
+ code,
242
+ a: MarkdownLink({
243
+ Link,
244
+ onLinkClick
196
245
  })
197
246
  };
198
247
  }, [
199
248
  renderMermaid,
200
- codeHighlighter
249
+ codeHighlighter,
250
+ Link,
251
+ onLinkClick
201
252
  ]);
202
253
  const normalizedActions = normalizeActions(actions);
203
254
  const showActions = Object.values(normalizedActions).some(Boolean) && !isStreaming;
@@ -219,6 +270,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
219
270
  textContent: content,
220
271
  messageId: messageId,
221
272
  onFeedback: onFeedback,
273
+ feedbackValue: feedbackValue,
222
274
  onRegenerate: onRegenerate,
223
275
  onDelete: onDelete,
224
276
  translate: translate
@@ -491,6 +543,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
491
543
  textContent: allTextContent,
492
544
  messageId: messageId,
493
545
  onFeedback: onFeedback,
546
+ feedbackValue: feedbackValue,
494
547
  onRegenerate: onRegenerate,
495
548
  onDelete: onDelete,
496
549
  translate: translate
@@ -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, translate }, ref) {
58
+ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messages, isStreaming, config, addToolApprovalResponse, onFeedback, onLinkClick, Link, feedbackValues, 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
@@ -189,6 +189,9 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
189
189
  actions: config?.actions,
190
190
  messageId: info.key,
191
191
  onFeedback: onFeedback,
192
+ onLinkClick: onLinkClick,
193
+ Link: Link,
194
+ feedbackValue: feedbackValues?.[info.key],
192
195
  onRegenerate: onRegenerate,
193
196
  onDelete: onDelete,
194
197
  translate: translate
@@ -23,7 +23,7 @@
23
23
  onUserMessage: 'Trigger when the user sends a message.',
24
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).',
25
25
  onError: 'Trigger on stream error.',
26
- onFeedback: 'Trigger when the user clicks thumbs up or down on a message.',
26
+ onFeedback: 'Trigger when the user clicks thumbs up or down on a message. Event contains messageId, messageContent and rating, where rating is `like`, `dislike`, or `default` when the user clicks the selected thumb again to clear the rating.',
27
27
  onRegenerate: 'Trigger when the user clicks regenerate on a message.',
28
28
  onDeleteMessage: 'Trigger when the user deletes a message.',
29
29
  onEditMessage: 'Trigger when the user edits and resubmits a message.',
@@ -32,7 +32,8 @@
32
32
  onSuggestionClick: 'Trigger when the user clicks a follow-up suggestion.',
33
33
  onTitleGenerated: 'Trigger when the agent generates a conversation title.',
34
34
  onSwitchChange: 'Trigger when the user toggles a sender switch.',
35
- onDataPart: 'Trigger when the agent sends a custom data part. Event contains type, data, and id.'
35
+ onDataPart: 'Trigger when the agent sends a custom data part. Event contains type, data, and id.',
36
+ onLinkClick: 'Trigger when the user clicks a link in a message. Event contains href and text. Wiring this event suppresses the default navigation for plain left clicks, so the app can open the target in place; modified clicks (new tab, middle click) always navigate.'
36
37
  },
37
38
  methods: {
38
39
  regenerate: 'Regenerate the last assistant message. Accepts optional args.messageId to regenerate a specific message.',
@@ -162,6 +162,10 @@
162
162
  "messages": {
163
163
  "type": "array"
164
164
  },
165
+ "feedbackValues": {
166
+ "type": "object",
167
+ "description": "Ratings already recorded, keyed by message id, each `like` or `dislike`. Restores the thumbs on a conversation the block did not rate this visit. A rating clicked this visit takes precedence."
168
+ },
165
169
  "conversationId": {
166
170
  "type": "string",
167
171
  "description": "Active conversation ID. When this value changes, messages are automatically cleared."
@@ -186,10 +190,17 @@
186
190
  "allowSpeech": { "type": "boolean", "default": false },
187
191
  "attachments": {
188
192
  "type": "object",
193
+ "description": "File attachments on the composer. When enabled, files can be picked with the paperclip, pasted from the clipboard (e.g. a screenshot) or dragged onto the composer; accept and maxSize apply to all three.",
189
194
  "properties": {
190
195
  "enabled": { "type": "boolean" },
191
- "accept": { "type": "string" },
192
- "maxSize": { "type": "number" },
196
+ "accept": {
197
+ "type": "string",
198
+ "description": "Comma-separated accept list, as for an <input type=\"file\"> — e.g. \"image/*,.pdf\". Applied to picked, pasted and dropped files alike."
199
+ },
200
+ "maxSize": {
201
+ "type": "number",
202
+ "description": "Maximum file size in bytes. Larger files are silently skipped."
203
+ },
193
204
  "s3PostPolicyRequestId": {
194
205
  "type": "string",
195
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."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/blocks-antd-x",
3
- "version": "5.5.0",
3
+ "version": "5.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy Ant Design X Blocks",
6
6
  "homepage": "https://lowdefy.com",
@@ -41,11 +41,14 @@
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.5.0",
45
- "@lowdefy/helpers": "5.5.0",
44
+ "@lowdefy/block-utils": "5.6.0",
45
+ "@lowdefy/helpers": "5.6.0",
46
46
  "ai": "6.0.176"
47
47
  },
48
48
  "devDependencies": {
49
+ "@lowdefy/block-dev-e2e": "5.6.0",
50
+ "@lowdefy/e2e-utils": "5.6.0",
51
+ "@playwright/test": "1.50.1",
49
52
  "@swc/cli": "0.8.0",
50
53
  "@swc/core": "1.15.18",
51
54
  "copyfiles": "2.4.1"
@@ -60,6 +63,8 @@
60
63
  "scripts": {
61
64
  "build": "swc src --out-dir dist --config-file ../../../../.swcrc --cli-config-file ../../../../.swc-cli.json && pnpm copyfiles",
62
65
  "clean": "rm -rf dist",
63
- "copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.yaml\" -e \"./src/**/*.snap\""
66
+ "copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.yaml\" -e \"./src/**/*.snap\"",
67
+ "e2e": "playwright test --config e2e/playwright.config.js",
68
+ "e2e:ui": "playwright test --config e2e/playwright.config.js --ui"
64
69
  }
65
70
  }