@lowdefy/blocks-antd-x 6.0.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 -774
- 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 +20 -4
- package/dist/blocks/AgentChat/formatToolResult.js +2 -1
- package/dist/blocks/AgentChat/meta.js +17 -1
- package/dist/blocks/AgentChat/replayableMessages.js +60 -0
- package/dist/blocks/AgentChat/schema.json +36 -2
- package/dist/blocks/AgentChat/tests/AgentChat.e2e.yaml +237 -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 +16 -13
|
@@ -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
|
|
@@ -154,7 +153,15 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
|
|
|
154
153
|
placement: 'end',
|
|
155
154
|
variant: config?.roles?.user?.variant ?? 'filled',
|
|
156
155
|
shape: config?.roles?.user?.shape ?? 'round',
|
|
157
|
-
|
|
156
|
+
styles: config?.roles?.user?.styles,
|
|
157
|
+
classNames: config?.roles?.user?.classNames,
|
|
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
|
+
})),
|
|
158
165
|
header: roleHeader(config?.roles?.user, 'agent.message.userHeader', translate),
|
|
159
166
|
editable: config?.editableMessages !== false ? {
|
|
160
167
|
onEditConfirm: (key, newContent)=>{
|
|
@@ -200,7 +207,15 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
|
|
|
200
207
|
style: {
|
|
201
208
|
maxWidth: '100%'
|
|
202
209
|
},
|
|
203
|
-
|
|
210
|
+
styles: config?.roles?.assistant?.styles,
|
|
211
|
+
classNames: config?.roles?.assistant?.classNames,
|
|
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
|
+
})),
|
|
204
219
|
header: roleHeader(config?.roles?.assistant, 'agent.message.assistantHeader', translate),
|
|
205
220
|
typing: config?.roles?.assistant?.typing ? config.roles.assistant.typing : {
|
|
206
221
|
effect: 'fade-in'
|
|
@@ -208,6 +223,7 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
|
|
|
208
223
|
contentRender: (content, info)=>{
|
|
209
224
|
const parts = partsMap.get(info.key);
|
|
210
225
|
return /*#__PURE__*/ React.createElement(MessageBubble, {
|
|
226
|
+
Icon: Icon,
|
|
211
227
|
content: content,
|
|
212
228
|
isStreaming: isStreaming,
|
|
213
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.',
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
16
|
+
// A reply cut off mid-stream leaves its tool call with no result, and UIMessage validation
|
|
17
|
+
// rejects the whole history for one such part: every later send fails, and a history stored
|
|
18
|
+
// that way fails on every load. Approval states stay; a pending approval is answerable on resume.
|
|
19
|
+
// A client-side tool the block is still running is in the same state, so its call id is in
|
|
20
|
+
// liveToolCallIds and the part is kept: its output is on the way.
|
|
21
|
+
const UNANSWERED = new Set([
|
|
22
|
+
'input-streaming',
|
|
23
|
+
'input-available'
|
|
24
|
+
]);
|
|
25
|
+
function isDeadToolPart({ part, liveToolCallIds }) {
|
|
26
|
+
const partType = part?.type;
|
|
27
|
+
const isTool = partType === 'dynamic-tool' || type.isString(partType) && partType.startsWith('tool-');
|
|
28
|
+
return isTool && UNANSWERED.has(part.state) && !liveToolCallIds.has(part.toolCallId);
|
|
29
|
+
}
|
|
30
|
+
function replayableMessages({ messages, liveToolCallIds = new Set() }) {
|
|
31
|
+
const list = messages ?? [];
|
|
32
|
+
let changed = false;
|
|
33
|
+
const result = [];
|
|
34
|
+
for (const message of list){
|
|
35
|
+
if (!type.isArray(message?.parts)) {
|
|
36
|
+
result.push(message);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const parts = message.parts.filter((part)=>!isDeadToolPart({
|
|
40
|
+
part,
|
|
41
|
+
liveToolCallIds
|
|
42
|
+
}));
|
|
43
|
+
// Only a step marker left is the empty shell onError already drops.
|
|
44
|
+
if (!parts.some((part)=>part?.type !== 'step-start')) {
|
|
45
|
+
changed = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (parts.length === message.parts.length) {
|
|
49
|
+
result.push(message);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
changed = true;
|
|
53
|
+
result.push({
|
|
54
|
+
...message,
|
|
55
|
+
parts
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return changed ? result : list;
|
|
59
|
+
}
|
|
60
|
+
export default replayableMessages;
|
|
@@ -137,6 +137,14 @@
|
|
|
137
137
|
"shape": {
|
|
138
138
|
"type": "string",
|
|
139
139
|
"enum": ["default", "round", "corner"]
|
|
140
|
+
},
|
|
141
|
+
"styles": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"description": "Inline styles per bubble part: root, content, body, header, footer, avatar, extra. The way to recolour a bubble: the filled and outlined colours come from the theme's colorPrimary and are not reachable from a stylesheet."
|
|
144
|
+
},
|
|
145
|
+
"classNames": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"description": "Class names per bubble part, same keys as styles."
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
},
|
|
@@ -152,6 +160,14 @@
|
|
|
152
160
|
"shape": {
|
|
153
161
|
"type": "string",
|
|
154
162
|
"enum": ["default", "round", "corner"]
|
|
163
|
+
},
|
|
164
|
+
"styles": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"description": "Inline styles per bubble part: root, content, body, header, footer, avatar, extra."
|
|
167
|
+
},
|
|
168
|
+
"classNames": {
|
|
169
|
+
"type": "object",
|
|
170
|
+
"description": "Class names per bubble part, same keys as styles."
|
|
155
171
|
}
|
|
156
172
|
}
|
|
157
173
|
}
|
|
@@ -175,9 +191,14 @@
|
|
|
175
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."
|
|
176
192
|
},
|
|
177
193
|
"sharedState": {
|
|
178
|
-
"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 }`.",
|
|
179
195
|
"type": "object"
|
|
180
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
|
+
},
|
|
181
202
|
"sender": {
|
|
182
203
|
"type": "object",
|
|
183
204
|
"properties": {
|
|
@@ -188,6 +209,14 @@
|
|
|
188
209
|
"default": "enter"
|
|
189
210
|
},
|
|
190
211
|
"allowSpeech": { "type": "boolean", "default": false },
|
|
212
|
+
"styles": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"description": "Inline styles per composer part: root, prefix, input, suffix, footer, switch, content. The way to recolour the composer border, which the theme sets with a precedence no page stylesheet beats."
|
|
215
|
+
},
|
|
216
|
+
"classNames": {
|
|
217
|
+
"type": "object",
|
|
218
|
+
"description": "Class names per composer part, same keys as styles."
|
|
219
|
+
},
|
|
191
220
|
"attachments": {
|
|
192
221
|
"type": "object",
|
|
193
222
|
"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.",
|
|
@@ -203,7 +232,7 @@
|
|
|
203
232
|
},
|
|
204
233
|
"uploadPolicyRequestId": {
|
|
205
234
|
"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."
|
|
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."
|
|
207
236
|
},
|
|
208
237
|
"downloadPolicyRequestId": {
|
|
209
238
|
"type": "string",
|
|
@@ -243,6 +272,11 @@
|
|
|
243
272
|
"title": { "type": "string" }
|
|
244
273
|
}
|
|
245
274
|
},
|
|
275
|
+
"loading": {
|
|
276
|
+
"type": "boolean",
|
|
277
|
+
"default": false,
|
|
278
|
+
"description": "The app is still loading this conversation's transcript. While true, a skeleton replaces the message area, suggestions are hidden and the composer is disabled, so nothing can be sent into a conversation whose messages have not synced yet."
|
|
279
|
+
},
|
|
246
280
|
"suggestions": {
|
|
247
281
|
"type": "array",
|
|
248
282
|
"items": {
|