@lowdefy/blocks-antd-x 6.1.0 → 7.1.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.
@@ -0,0 +1,32 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import React from 'react';
16
+ import { FloatButton } from 'antd';
17
+ // Shared by the loading fallback and the drawer, so the button painted before the
18
+ // implementation loads is the same one that replaces it.
19
+ function DrawerLauncher({ blockId, config, Icon, onClick }) {
20
+ return /*#__PURE__*/ React.createElement(FloatButton, {
21
+ icon: /*#__PURE__*/ React.createElement(Icon, {
22
+ blockId: `${blockId}_launcher_icon`,
23
+ properties: {
24
+ name: 'message',
25
+ title: ''
26
+ }
27
+ }),
28
+ onClick: onClick,
29
+ tooltip: config?.title ?? 'Chat'
30
+ });
31
+ }
32
+ export default DrawerLauncher;
@@ -13,14 +13,15 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import React, { useState } from 'react';
16
- import { Drawer, FloatButton } from 'antd';
17
- import { MessageOutlined } from '@ant-design/icons';
18
- function DrawerWrapper({ config, children }) {
19
- const [open, setOpen] = useState(false);
20
- return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(FloatButton, {
21
- icon: /*#__PURE__*/ React.createElement(MessageOutlined, null),
22
- onClick: ()=>setOpen(true),
23
- tooltip: config?.title ?? 'Chat'
16
+ import { Drawer } from 'antd';
17
+ import DrawerLauncher from './DrawerLauncher.js';
18
+ function DrawerWrapper({ blockId, config, defaultOpen, children, Icon }) {
19
+ const [open, setOpen] = useState(defaultOpen);
20
+ return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(DrawerLauncher, {
21
+ blockId: blockId,
22
+ config: config,
23
+ Icon: Icon,
24
+ onClick: ()=>setOpen(true)
24
25
  }), /*#__PURE__*/ React.createElement(Drawer, {
25
26
  title: config?.title ?? 'Chat',
26
27
  placement: config?.placement ?? 'right',
@@ -13,10 +13,10 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { DefaultChatTransport } from 'ai';
16
- function createLowdefyChatTransport({ pageId, agentId, conversationId, urlQuery, sharedStateRef }) {
16
+ function createLowdefyChatTransport({ basePath = '', pageId, agentId, conversationId, urlQuery, pathParams, sharedStateRef, sharedStateReadOnlyRef }) {
17
17
  // The server route takes the last path segment as the agentId — module-scoped
18
18
  // agent ids contain '/', so encode the agentId into a single segment.
19
- const base = `/api/agent/${pageId}/${encodeURIComponent(agentId)}`;
19
+ const base = `${basePath}/api/agent/${pageId}/${encodeURIComponent(agentId)}`;
20
20
  const api = conversationId ? `${base}?conversationId=${encodeURIComponent(conversationId)}` : base;
21
21
  return new DefaultChatTransport({
22
22
  api,
@@ -27,8 +27,14 @@ function createLowdefyChatTransport({ pageId, agentId, conversationId, urlQuery,
27
27
  ...urlQuery ? {
28
28
  urlQuery
29
29
  } : {},
30
+ ...pathParams ? {
31
+ pathParams
32
+ } : {},
30
33
  ...sharedState ? {
31
34
  sharedState
35
+ } : {},
36
+ ...sharedStateReadOnlyRef?.current ? {
37
+ sharedStateReadOnly: true
32
38
  } : {}
33
39
  };
34
40
  }
@@ -12,31 +12,14 @@
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, { useMemo } from 'react';
15
+ */ import React, { Suspense, lazy, useEffect, useMemo, useState } from 'react';
16
16
  import Markdown from '@ant-design/x-markdown';
17
- import { Actions, CodeHighlighter, FileCard, Mermaid, Sources, ThoughtChain, Think } from '@ant-design/x';
18
- import { DeleteOutlined, ReloadOutlined, RobotOutlined } from '@ant-design/icons';
17
+ import { Actions, FileCard, Sources, ThoughtChain, Think } from '@ant-design/x';
18
+ import { getSafeHref } from '@lowdefy/block-utils';
19
19
  import { getFileCardType, getFileCardIcon, getFileName } from './fileCardUtils.js';
20
20
  import formatToolResult from './formatToolResult.js';
21
21
  import { getToolInfo, partCategory } from './messageParts.js';
22
22
  import ToolApproval from './ToolApproval.js';
23
- // Module-level singleton for the LaTeX marked extension.
24
- // Loaded once on first use — the Latex plugin from @ant-design/x-markdown
25
- // includes KaTeX CSS import and handles $...$ inline and $$...$$ display math.
26
- let latexConfig = null;
27
- function getLatexConfig() {
28
- if (latexConfig) return latexConfig;
29
- try {
30
- // eslint-disable-next-line global-require
31
- const Latex = require('@ant-design/x-markdown/plugins/Latex').default;
32
- latexConfig = {
33
- extensions: Latex()
34
- };
35
- } catch {
36
- latexConfig = {};
37
- }
38
- return latexConfig;
39
- }
40
23
  // Renders a code block as plain text without syntax highlighting or mermaid rendering.
41
24
  function PlainCodeBlock({ children, block, lang }) {
42
25
  if (!block) {
@@ -46,31 +29,97 @@ function PlainCodeBlock({ children, block, lang }) {
46
29
  className: lang ? `language-${lang}` : undefined
47
30
  }, children));
48
31
  }
32
+ // Mermaid and the Prism highlighter are most of AgentChat's weight, and a plain reply
33
+ // uses neither. Deep paths keep them out of the chunk that imports the @ant-design/x
34
+ // barrel; they load the first time a message renders a fence that needs them. A chunk
35
+ // that fails to load (a deploy replaced it) degrades to plain code, not a broken chat.
36
+ function lazyCodeRenderer(load, name) {
37
+ return /*#__PURE__*/ lazy(()=>load().catch((error)=>{
38
+ console.warn(`AgentChat could not load ${name}; showing plain code.`, error);
39
+ return {
40
+ default: function PlainCode({ children, lang }) {
41
+ return /*#__PURE__*/ React.createElement(PlainCodeBlock, {
42
+ block: true,
43
+ lang: lang
44
+ }, children);
45
+ }
46
+ };
47
+ }));
48
+ }
49
+ const Mermaid = lazyCodeRenderer(()=>import('@ant-design/x/es/mermaid/index.js'), 'Mermaid');
50
+ const CodeHighlighter = lazyCodeRenderer(()=>import('@ant-design/x/es/code-highlighter/index.js'), 'CodeHighlighter');
49
51
  // Renders code blocks with syntax highlighting via CodeHighlighter and
50
52
  // mermaid diagrams via the Mermaid component from @ant-design/x.
53
+ // Each lazy renderer sits in its own Suspense, so a first load shows the plain code in
54
+ // place rather than suspending the whole chat.
51
55
  function RichCodeBlock({ renderMermaid, codeHighlighter }) {
52
56
  return function CodeBlock({ children, block, lang }) {
53
57
  if (!block) {
54
58
  return /*#__PURE__*/ React.createElement("code", null, children);
55
59
  }
60
+ const plain = /*#__PURE__*/ React.createElement(PlainCodeBlock, {
61
+ block: true,
62
+ lang: lang
63
+ }, children);
56
64
  if (renderMermaid && lang === 'mermaid') {
57
65
  return /*#__PURE__*/ React.createElement("div", {
58
66
  style: {
59
67
  width: '100%'
60
68
  }
61
- }, /*#__PURE__*/ React.createElement(Mermaid, null, children));
69
+ }, /*#__PURE__*/ React.createElement(Suspense, {
70
+ fallback: plain
71
+ }, /*#__PURE__*/ React.createElement(Mermaid, null, children)));
62
72
  }
63
73
  if (codeHighlighter) {
64
- return /*#__PURE__*/ React.createElement(CodeHighlighter, {
74
+ return /*#__PURE__*/ React.createElement(Suspense, {
75
+ fallback: plain
76
+ }, /*#__PURE__*/ React.createElement(CodeHighlighter, {
65
77
  lang: lang,
66
78
  prismLightMode: false
67
- }, children);
79
+ }, children));
68
80
  }
69
- return /*#__PURE__*/ React.createElement("pre", null, /*#__PURE__*/ React.createElement("code", {
70
- className: lang ? `language-${lang}` : undefined
71
- }, children));
81
+ return plain;
72
82
  };
73
83
  }
84
+ // The LaTeX marked extension (KaTeX and its CSS) loads only for chats that set
85
+ // messageDisplay.renderLatex. Markdown renders without it until it arrives, then re-renders
86
+ // with it. One config object is shared so every bubble passes Markdown the same reference.
87
+ let latexConfig = null;
88
+ let latexConfigPromise = null;
89
+ function loadLatexConfig() {
90
+ if (!latexConfigPromise) {
91
+ latexConfigPromise = import('@ant-design/x-markdown/plugins/Latex/index.js').then(({ default: Latex })=>{
92
+ latexConfig = {
93
+ extensions: Latex()
94
+ };
95
+ return latexConfig;
96
+ }, (error)=>{
97
+ // Cleared so a later chat mount tries again.
98
+ latexConfigPromise = null;
99
+ console.warn('AgentChat could not load LaTeX rendering; showing the source.', error);
100
+ return null;
101
+ });
102
+ }
103
+ return latexConfigPromise;
104
+ }
105
+ function useLatexConfig(renderLatex) {
106
+ const [loadedConfig, setLoadedConfig] = useState(latexConfig);
107
+ useEffect(()=>{
108
+ if (!renderLatex || loadedConfig) return undefined;
109
+ let mounted = true;
110
+ loadLatexConfig().then((config)=>{
111
+ if (mounted && config) setLoadedConfig(config);
112
+ });
113
+ return ()=>{
114
+ mounted = false;
115
+ };
116
+ }, [
117
+ renderLatex,
118
+ loadedConfig
119
+ ]);
120
+ if (!renderLatex) return undefined;
121
+ return loadedConfig ?? undefined;
122
+ }
74
123
  function summarizeToolOutput(output, translate) {
75
124
  if (output === null || output === undefined) return translate('agent.toolResult.completedNoData');
76
125
  if (Array.isArray(output)) {
@@ -115,7 +164,8 @@ function linkText(children) {
115
164
  // new tab for the same reason. Both still report the click, so an app that handles
116
165
  // onLinkClick can show the target in place instead.
117
166
  function MarkdownLink({ Link, onLinkClick }) {
118
- return function MarkdownAnchor({ href, children, ...props }) {
167
+ return function MarkdownAnchor({ href: rawHref, children, ...props }) {
168
+ const href = getSafeHref(rawHref);
119
169
  if (!href) return /*#__PURE__*/ React.createElement("span", props, children);
120
170
  const text = linkText(children);
121
171
  const handleClick = (domEvent)=>onLinkClick?.({
@@ -124,7 +174,7 @@ function MarkdownLink({ Link, onLinkClick }) {
124
174
  domEvent
125
175
  });
126
176
  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.
177
+ // A fragment moves within the document, which the router cannot do, so it keeps the anchor.
128
178
  const inApp = !external && href.startsWith('/') && !href.includes('#');
129
179
  if (!inApp || !Link) {
130
180
  return /*#__PURE__*/ React.createElement("a", {
@@ -135,14 +185,9 @@ function MarkdownLink({ Link, onLinkClick }) {
135
185
  onClick: handleClick
136
186
  }, children);
137
187
  }
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
188
  return /*#__PURE__*/ React.createElement(Link, {
143
189
  ...props,
144
- pageId: pathname,
145
- urlQuery: Object.fromEntries(new URLSearchParams(search)),
190
+ url: href,
146
191
  onClick: handleClick
147
192
  }, children);
148
193
  };
@@ -157,7 +202,7 @@ function normalizeActions(actions) {
157
202
  }
158
203
  return actions ?? {};
159
204
  }
160
- function BubbleActions({ actions, textContent, messageId, onFeedback, feedbackValue, onRegenerate, onDelete, translate }) {
205
+ function BubbleActions({ Icon, actions, textContent, messageId, onFeedback, feedbackValue, onRegenerate, onDelete, translate }) {
161
206
  const normalized = normalizeActions(actions);
162
207
  const items = [];
163
208
  if (normalized.copy) {
@@ -185,7 +230,13 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, feedbackVa
185
230
  if (normalized.regenerate) {
186
231
  items.push({
187
232
  key: 'regenerate',
188
- icon: /*#__PURE__*/ React.createElement(ReloadOutlined, null),
233
+ icon: /*#__PURE__*/ React.createElement(Icon, {
234
+ blockId: `${messageId}_regenerate_icon`,
235
+ properties: {
236
+ name: 'refresh',
237
+ title: ''
238
+ }
239
+ }),
189
240
  label: translate('agent.message.regenerate'),
190
241
  onItemClick: ()=>onRegenerate?.({
191
242
  messageId
@@ -195,7 +246,13 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, feedbackVa
195
246
  if (normalized.delete) {
196
247
  items.push({
197
248
  key: 'delete',
198
- icon: /*#__PURE__*/ React.createElement(DeleteOutlined, null),
249
+ icon: /*#__PURE__*/ React.createElement(Icon, {
250
+ blockId: `${messageId}_delete_icon`,
251
+ properties: {
252
+ name: 'delete',
253
+ title: ''
254
+ }
255
+ }),
199
256
  label: translate('agent.message.delete'),
200
257
  danger: true,
201
258
  onItemClick: ()=>onDelete?.({
@@ -223,7 +280,7 @@ function SourcesDisplay({ sourceParts, config, translate }) {
223
280
  expandIconPosition: config?.sourcesDisplay?.expandIconPosition ?? 'end'
224
281
  });
225
282
  }
226
- function MessageBubble({ content, isStreaming, parts, config, addToolApprovalResponse, actions, messageId, onFeedback, feedbackValue, onLinkClick, Link, onRegenerate, onDelete, translate }) {
283
+ function MessageBubble({ Icon, content, isStreaming, parts, config, addToolApprovalResponse, actions, messageId, onFeedback, feedbackValue, onLinkClick, Link, onRegenerate, onDelete, translate }) {
227
284
  const showThoughtChain = config?.showThoughtChain !== false;
228
285
  const showReasoning = config?.showReasoning !== false;
229
286
  const reasoningDisplay = config?.reasoningDisplay ?? 'interleaved';
@@ -231,7 +288,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
231
288
  const renderMermaid = config?.renderMermaid !== false;
232
289
  const codeHighlighter = config?.codeHighlighter !== false;
233
290
  const renderLatex = config?.renderLatex ?? false;
234
- const markdownConfig = renderLatex ? getLatexConfig() : undefined;
291
+ const markdownConfig = useLatexConfig(renderLatex);
235
292
  const markdownComponents = useMemo(()=>{
236
293
  const code = !renderMermaid && !codeHighlighter ? PlainCodeBlock : RichCodeBlock({
237
294
  renderMermaid,
@@ -266,6 +323,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
266
323
  config: config,
267
324
  translate: translate
268
325
  }), showActions && /*#__PURE__*/ React.createElement(BubbleActions, {
326
+ Icon: Icon,
269
327
  actions: normalizedActions,
270
328
  textContent: content,
271
329
  messageId: messageId,
@@ -464,7 +522,13 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
464
522
  collapsible: true
465
523
  } : {},
466
524
  ...isSubAgent ? {
467
- icon: /*#__PURE__*/ React.createElement(RobotOutlined, null)
525
+ icon: /*#__PURE__*/ React.createElement(Icon, {
526
+ blockId: `${tool.toolCallId}_icon`,
527
+ properties: {
528
+ name: 'bot',
529
+ title: ''
530
+ }
531
+ })
468
532
  } : {},
469
533
  ...status === 'loading' ? {
470
534
  blink: true
@@ -539,6 +603,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
539
603
  config: config,
540
604
  translate: translate
541
605
  }), showActions && /*#__PURE__*/ React.createElement(BubbleActions, {
606
+ Icon: Icon,
542
607
  actions: normalizedActions,
543
608
  textContent: allTextContent,
544
609
  messageId: messageId,
@@ -15,7 +15,6 @@
15
15
  */ import React from 'react';
16
16
  import { Bubble, FileCard } from '@ant-design/x';
17
17
  import { Avatar } from 'antd';
18
- import { RobotOutlined, UserOutlined } from '@ant-design/icons';
19
18
  import { getFileCardType, getFileCardIcon, getFileName } from './fileCardUtils.js';
20
19
  import { hasVisibleContent } from './messageParts.js';
21
20
  import MessageBubble from './MessageBubble.js';
@@ -56,7 +55,7 @@ function ThinkingBubbleContent({ bubbleId, config }) {
56
55
  label: label
57
56
  });
58
57
  }
59
- const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messages, isStreaming, config, welcome, onWelcomePromptFill, addToolApprovalResponse, onFeedback, onLinkClick, Link, feedbackValues, onRegenerate, onDelete, onEditMessage, translate }, ref) {
58
+ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ Icon, messages, isStreaming, config, welcome, onWelcomePromptFill, addToolApprovalResponse, onFeedback, onLinkClick, Link, feedbackValues, onRegenerate, onDelete, onEditMessage, translate }, ref) {
60
59
  // Build a lookup map for message parts.
61
60
  // Bubble.List's contentRender callback only receives (content, info) where info.key is the
62
61
  // item key — it does not receive the full message object with its parts array. This map
@@ -156,7 +155,13 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
156
155
  shape: config?.roles?.user?.shape ?? 'round',
157
156
  styles: config?.roles?.user?.styles,
158
157
  classNames: config?.roles?.user?.classNames,
159
- avatar: roleAvatar(config?.roles?.user, /*#__PURE__*/ React.createElement(UserOutlined, null)),
158
+ avatar: roleAvatar(config?.roles?.user, /*#__PURE__*/ React.createElement(Icon, {
159
+ blockId: "agent_chat_user_avatar",
160
+ properties: {
161
+ name: 'user',
162
+ title: ''
163
+ }
164
+ })),
160
165
  header: roleHeader(config?.roles?.user, 'agent.message.userHeader', translate),
161
166
  editable: config?.editableMessages !== false ? {
162
167
  onEditConfirm: (key, newContent)=>{
@@ -204,7 +209,13 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
204
209
  },
205
210
  styles: config?.roles?.assistant?.styles,
206
211
  classNames: config?.roles?.assistant?.classNames,
207
- avatar: roleAvatar(config?.roles?.assistant, /*#__PURE__*/ React.createElement(RobotOutlined, null)),
212
+ avatar: roleAvatar(config?.roles?.assistant, /*#__PURE__*/ React.createElement(Icon, {
213
+ blockId: "agent_chat_assistant_avatar",
214
+ properties: {
215
+ name: 'bot',
216
+ title: ''
217
+ }
218
+ })),
208
219
  header: roleHeader(config?.roles?.assistant, 'agent.message.assistantHeader', translate),
209
220
  typing: config?.roles?.assistant?.typing ? config.roles.assistant.typing : {
210
221
  effect: 'fade-in'
@@ -212,6 +223,7 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
212
223
  contentRender: (content, info)=>{
213
224
  const parts = partsMap.get(info.key);
214
225
  return /*#__PURE__*/ React.createElement(MessageBubble, {
226
+ Icon: Icon,
215
227
  content: content,
216
228
  isStreaming: isStreaming,
217
229
  parts: parts,
@@ -13,6 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import React, { useState } from 'react';
16
+ import { getSafeHref } from '@lowdefy/block-utils';
16
17
  const URL_REGEX = /^https?:\/\/\S+$/;
17
18
  const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})/;
18
19
  function isUrl(value) {
@@ -52,7 +53,7 @@ function formatValue(value, depth, translate) {
52
53
  }
53
54
  if (isUrl(value)) {
54
55
  return /*#__PURE__*/ React.createElement("a", {
55
- href: value,
56
+ href: getSafeHref(value),
56
57
  target: "_blank",
57
58
  rel: "noopener noreferrer",
58
59
  style: {
@@ -14,7 +14,22 @@
14
14
  limitations under the License.
15
15
  */ export default {
16
16
  category: 'display',
17
- icons: [],
17
+ actions: [
18
+ 'Request',
19
+ 'SetState'
20
+ ],
21
+ operators: [
22
+ '_event'
23
+ ],
24
+ icons: [
25
+ 'refresh',
26
+ 'delete',
27
+ 'bot',
28
+ 'message',
29
+ 'user',
30
+ 'attach',
31
+ 'close'
32
+ ],
18
33
  valueType: null,
19
34
  events: {
20
35
  onMessageComplete: 'Trigger when a message finishes streaming.',
@@ -39,6 +54,7 @@
39
54
  regenerate: 'Regenerate the last assistant message. Accepts optional args.messageId to regenerate a specific message.',
40
55
  setMessages: 'Replace the message list. Accepts args.messages array.',
41
56
  sendMessage: 'Send a message programmatically. Accepts args.text string.',
57
+ setInput: 'Fill the composer without sending. Accepts args.text string; anything else empties it.',
42
58
  clearMessages: 'Clear all messages from the chat.',
43
59
  deleteMessage: 'Delete a specific message. Accepts args.messageId string.',
44
60
  stop: 'Stop the current streaming response.',
@@ -191,9 +191,14 @@
191
191
  "description": "URL query parameters to send with each agent request. Available server-side in onFinish hook payload and via _payload operator in agent properties."
192
192
  },
193
193
  "sharedState": {
194
- "description": "When set to a non-empty object, the object is sent to the agent each turn as read context and the agent can write to any top-level key via the update-page-state tool. Typically set to `{ _state: true }` to expose the whole page state, or a curated shape like `{ legal: _state.legal_name }`.",
194
+ "description": "When set to a non-empty object, the object is sent to the agent each turn as read context and the agent can write to any top-level key via the update-page-state tool (unless `sharedStateReadOnly` is true). Typically set to `{ _state: true }` to expose the whole page state, or a curated shape like `{ legal: _state.legal_name }`.",
195
195
  "type": "object"
196
196
  },
197
+ "sharedStateReadOnly": {
198
+ "type": "boolean",
199
+ "default": false,
200
+ "description": "When true, `sharedState` is sent to the agent as read-only context: the update-page-state tool is not offered, so the agent cannot write page state."
201
+ },
197
202
  "sender": {
198
203
  "type": "object",
199
204
  "properties": {
@@ -227,7 +232,7 @@
227
232
  },
228
233
  "uploadPolicyRequestId": {
229
234
  "type": "string",
230
- "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."
235
+ "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. Each uploaded file part keeps its storage key as providerMetadata.lowdefy.key, which a tool endpoint reads with _agent: files."
231
236
  },
232
237
  "downloadPolicyRequestId": {
233
238
  "type": "string",
@@ -337,3 +337,22 @@ blocks:
337
337
  parts:
338
338
  - type: text
339
339
  text: Loaded answer.
340
+
341
+ # Both post to the agent route, which the spec intercepts to read the request body.
342
+ - id: chat_shared_read_only
343
+ type: AgentChat
344
+ properties:
345
+ agentId: test_agent
346
+ height: 200
347
+ conversationId: shared-read-only
348
+ sharedStateReadOnly: true
349
+ sharedState:
350
+ space: PEL
351
+ - id: chat_shared_writable
352
+ type: AgentChat
353
+ properties:
354
+ agentId: test_agent
355
+ height: 200
356
+ conversationId: shared-writable
357
+ sharedState:
358
+ space: PEL
@@ -14,9 +14,24 @@
14
14
  limitations under the License.
15
15
  */ import React from 'react';
16
16
  import { Conversations } from '@ant-design/x';
17
- import { Button } from 'antd';
17
+ import { Button, Spin } from 'antd';
18
18
  function AgentConversations({ blockId, methods, properties }) {
19
- const { items, activeKey, menu, creation, groupable, width } = properties;
19
+ const { items, activeKey, loadingKey, menu, creation, groupable, width } = properties;
20
+ // Conversations has no loading state of its own, so a loading row swaps its icon slot
21
+ // for a spinner. aria-busy and data-loading reach the <li> through the item's DOM attrs.
22
+ const conversationItems = (items ?? []).map((item)=>{
23
+ if (item.loading !== true && item.key !== loadingKey) {
24
+ return item;
25
+ }
26
+ return {
27
+ ...item,
28
+ icon: /*#__PURE__*/ React.createElement(Spin, {
29
+ size: "small"
30
+ }),
31
+ 'aria-busy': true,
32
+ 'data-loading': true
33
+ };
34
+ });
20
35
  function handleNew() {
21
36
  methods.triggerEvent({
22
37
  name: 'onNew',
@@ -64,7 +79,7 @@ function AgentConversations({ blockId, methods, properties }) {
64
79
  block: true,
65
80
  onClick: handleNew
66
81
  }, "+ New Chat")), /*#__PURE__*/ React.createElement(Conversations, {
67
- items: items ?? [],
82
+ items: conversationItems,
68
83
  activeKey: activeKey,
69
84
  onActiveChange: (key)=>{
70
85
  methods.triggerEvent({
@@ -26,11 +26,15 @@
26
26
  properties: {
27
27
  items: {
28
28
  type: 'array',
29
- description: 'Conversation list. Each item: { key, label, icon?, disabled?, timestamp?, group? }.'
29
+ description: 'Conversation list. Each item: { key, label, icon?, disabled?, loading?, timestamp?, group? }.'
30
30
  },
31
31
  activeKey: {
32
32
  type: 'string',
33
33
  description: 'Currently selected conversation key.'
34
+ },
35
+ loadingKey: {
36
+ type: 'string',
37
+ description: 'Key of a conversation that is still loading. That row shows a spinner in place of its icon.'
34
38
  }
35
39
  }
36
40
  }
@@ -10,6 +10,10 @@
10
10
  "label": { "type": "string" },
11
11
  "icon": {},
12
12
  "disabled": { "type": "boolean" },
13
+ "loading": {
14
+ "type": "boolean",
15
+ "description": "Show a spinner in place of this row's icon."
16
+ },
13
17
  "timestamp": { "type": "number" },
14
18
  "group": { "type": "string" }
15
19
  }
@@ -17,6 +21,10 @@
17
21
  "default": []
18
22
  },
19
23
  "activeKey": { "type": "string" },
24
+ "loadingKey": {
25
+ "type": "string",
26
+ "description": "Key of a conversation that is still loading, for example while its transcript is read after a select. That row shows a spinner in place of its icon."
27
+ },
20
28
  "menu": {
21
29
  "type": "array",
22
30
  "items": {
@@ -0,0 +1,69 @@
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-conversations
16
+ type: PageHeaderMenu
17
+ properties:
18
+ title: AgentConversations
19
+ events:
20
+ onInit:
21
+ - id: init
22
+ type: SetState
23
+ params:
24
+ active: conv-1
25
+ blocks:
26
+ # Selecting a row marks it as loading until finish_loading clears loadingKey, standing in
27
+ # for an app that reads the transcript on select.
28
+ - id: conversations
29
+ type: AgentConversations
30
+ properties:
31
+ activeKey:
32
+ _state: active
33
+ loadingKey:
34
+ _state: loading
35
+ items:
36
+ - key: conv-1
37
+ label: First conversation
38
+ - key: conv-2
39
+ label: Second conversation
40
+ - key: conv-3
41
+ label: Generating a title
42
+ loading: true
43
+ events:
44
+ onSelect:
45
+ - id: select
46
+ type: SetState
47
+ params:
48
+ active:
49
+ _event: key
50
+ loading:
51
+ _event: key
52
+ - id: finish_loading
53
+ type: Button
54
+ properties:
55
+ title: Finish loading
56
+ events:
57
+ onClick:
58
+ - id: clear_loading
59
+ type: SetState
60
+ params:
61
+ loading: null
62
+ - id: readout_active
63
+ type: Html
64
+ properties:
65
+ html:
66
+ _string.concat:
67
+ - _if_none:
68
+ - _state: active
69
+ - ''