@lowdefy/blocks-antd-x 6.1.0 → 7.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.
- package/dist/blocks/AgentChat/AgentChat.js +38 -889
- package/dist/blocks/AgentChat/AgentChat.lazy.js +933 -0
- package/dist/blocks/AgentChat/DrawerLauncher.js +32 -0
- package/dist/blocks/AgentChat/DrawerWrapper.js +9 -8
- package/dist/blocks/AgentChat/LowdefyChatTransport.js +8 -2
- package/dist/blocks/AgentChat/MessageBubble.js +105 -40
- package/dist/blocks/AgentChat/MessageList.js +16 -4
- package/dist/blocks/AgentChat/formatToolResult.js +2 -1
- package/dist/blocks/AgentChat/meta.js +17 -1
- package/dist/blocks/AgentChat/schema.json +7 -2
- package/dist/blocks/AgentChat/tests/AgentChat.e2e.yaml +19 -0
- package/dist/blocks/AgentConversations/AgentConversations.js +18 -3
- package/dist/blocks/AgentConversations/meta.js +5 -1
- package/dist/blocks/AgentConversations/schema.json +8 -0
- package/dist/blocks/AgentConversations/tests/AgentConversations.e2e.yaml +69 -0
- package/dist/blocks/IntentInput/IntentInput.js +230 -0
- package/dist/blocks/IntentInput/IntentInput.module.css +159 -0
- package/dist/blocks/IntentInput/answerScores.js +34 -0
- package/dist/blocks/IntentInput/gallery.yaml +81 -0
- package/dist/blocks/IntentInput/meta.js +164 -0
- package/dist/blocks/IntentInput/nextIntentState.js +62 -0
- package/dist/blocks.js +1 -0
- package/dist/metas.js +1 -0
- package/dist/types.js +3 -18
- package/package.json +14 -12
|
@@ -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
|
|
17
|
-
import
|
|
18
|
-
function DrawerWrapper({ config, children }) {
|
|
19
|
-
const [open, setOpen] = useState(
|
|
20
|
-
return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 =
|
|
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,
|
|
18
|
-
import {
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
+
- ''
|