@truedat/ai 8.4.8 → 8.4.9
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/package.json +3 -3
- package/src/api.js +2 -0
- package/src/components/StructureSuggestions.js +147 -0
- package/src/components/ThinkingOutLoud.js +157 -0
- package/src/components/__tests__/StructureSuggestions.spec.js +290 -0
- package/src/components/assistant/AssistantChat.js +245 -60
- package/src/components/assistant/__tests__/AssistantChat.spec.js +79 -0
- package/src/components/assistant/__tests__/__snapshots__/AssistantChat.spec.js.snap +34 -11
- package/src/components/index.js +2 -1
- package/src/components/structureSuggestionColumns.js +44 -0
- package/src/hooks/useAgentLayerRun.js +9 -0
- package/src/styles/assistant.less +194 -9
|
@@ -1,33 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useCallback,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
} from "react";
|
|
2
9
|
import PropTypes from "prop-types";
|
|
3
10
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
11
|
import { Icon } from "semantic-ui-react";
|
|
5
12
|
import { MarkdownReader } from "@truedat/core/components";
|
|
13
|
+
import ThinkingOutLoud, { getSourceLabel } from "../ThinkingOutLoud";
|
|
6
14
|
|
|
7
15
|
const SCROLL_AT_BOTTOM_THRESHOLD = 50;
|
|
8
16
|
|
|
17
|
+
const SCROLL_OVERFLOW_EPSILON = 4;
|
|
18
|
+
const MAX_INPUT_LINES = 10;
|
|
19
|
+
|
|
9
20
|
function getVisibleMessages(conversation) {
|
|
10
21
|
if (!conversation || conversation.length === 0) return [];
|
|
11
|
-
|
|
12
|
-
for (let i = conversation.length - 1; i >= 0; i--) {
|
|
13
|
-
const eventType = conversation[i].eventType;
|
|
14
|
-
if (eventType === "request" || eventType === "response") {
|
|
15
|
-
lastRequestOrResponseIndex = i;
|
|
16
|
-
break;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return conversation
|
|
20
|
-
.map((item, idx) => ({ ...item, index: idx }))
|
|
21
|
-
.filter(
|
|
22
|
-
(item) =>
|
|
23
|
-
item.eventType === "request" ||
|
|
24
|
-
item.eventType === "response" ||
|
|
25
|
-
(item.eventType === "log" && item.index > lastRequestOrResponseIndex)
|
|
26
|
-
);
|
|
22
|
+
return conversation.map((item, idx) => ({ ...item, index: idx }));
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
function getContentFromPayload(event, payload) {
|
|
30
|
-
|
|
26
|
+
const priority = payload?.priority ?? null;
|
|
27
|
+
const source = payload?.source ?? null;
|
|
28
|
+
if (!payload || typeof payload !== "object")
|
|
29
|
+
return { content: "", isError: false, priority, source };
|
|
31
30
|
switch (event) {
|
|
32
31
|
case "stream": {
|
|
33
32
|
const c = payload.content;
|
|
@@ -37,7 +36,7 @@ function getContentFromPayload(event, payload) {
|
|
|
37
36
|
: c && typeof c === "object" && typeof c.message === "string"
|
|
38
37
|
? c.message
|
|
39
38
|
: "";
|
|
40
|
-
return { content: streamChunk, isError: false };
|
|
39
|
+
return { content: streamChunk, isError: false, priority, source };
|
|
41
40
|
}
|
|
42
41
|
case "error": {
|
|
43
42
|
const err = payload.error;
|
|
@@ -45,7 +44,7 @@ function getContentFromPayload(event, payload) {
|
|
|
45
44
|
err && typeof err === "object"
|
|
46
45
|
? err.message ?? err.reason ?? JSON.stringify(err)
|
|
47
46
|
: String(err ?? "");
|
|
48
|
-
return { content: errMsg, isError: true };
|
|
47
|
+
return { content: errMsg, isError: true, priority, source };
|
|
49
48
|
}
|
|
50
49
|
case "log": {
|
|
51
50
|
const log = payload.content;
|
|
@@ -53,14 +52,14 @@ function getContentFromPayload(event, payload) {
|
|
|
53
52
|
log && typeof log === "object"
|
|
54
53
|
? log.message ?? log.level ?? JSON.stringify(log)
|
|
55
54
|
: String(log ?? "");
|
|
56
|
-
return { content: logMsg, isError: false };
|
|
55
|
+
return { content: logMsg, isError: false, priority, source };
|
|
57
56
|
}
|
|
58
57
|
default:
|
|
59
|
-
return { content: "", isError: false };
|
|
58
|
+
return { content: "", isError: false, priority, source };
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
function appendStreamChunk(prev, source, chunk) {
|
|
62
|
+
function appendStreamChunk(prev, source, chunk, priority) {
|
|
64
63
|
const last = prev[prev.length - 1];
|
|
65
64
|
const sameSource =
|
|
66
65
|
last?.eventType === "response" &&
|
|
@@ -80,10 +79,58 @@ function appendStreamChunk(prev, source, chunk) {
|
|
|
80
79
|
content: chunk || " ",
|
|
81
80
|
streaming: true,
|
|
82
81
|
source,
|
|
82
|
+
priority,
|
|
83
83
|
},
|
|
84
84
|
];
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function isThinkingStreamMessage(msg) {
|
|
88
|
+
return (
|
|
89
|
+
msg.eventType === "log" ||
|
|
90
|
+
(msg.eventType === "error" && Number(msg.priority) === 2)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Converts a flat list of visible messages into render items:
|
|
96
|
+
* consecutive log messages (and inline thinking errors) are bundled into a "thinking-block".
|
|
97
|
+
*/
|
|
98
|
+
function buildRenderItems(messages) {
|
|
99
|
+
const result = [];
|
|
100
|
+
let logBuffer = [];
|
|
101
|
+
let startIdx = null;
|
|
102
|
+
messages.forEach((msg) => {
|
|
103
|
+
if (isThinkingStreamMessage(msg)) {
|
|
104
|
+
if (startIdx === null) startIdx = msg.index;
|
|
105
|
+
logBuffer.push(msg);
|
|
106
|
+
} else {
|
|
107
|
+
if (logBuffer.length > 0) {
|
|
108
|
+
const isFollowedByP0 = msg.eventType === "response" && msg.priority === 0;
|
|
109
|
+
result.push({
|
|
110
|
+
type: "thinking-block",
|
|
111
|
+
items: logBuffer,
|
|
112
|
+
active: false,
|
|
113
|
+
collapsed: isFollowedByP0,
|
|
114
|
+
key: `thinking-${startIdx}`,
|
|
115
|
+
});
|
|
116
|
+
logBuffer = [];
|
|
117
|
+
startIdx = null;
|
|
118
|
+
}
|
|
119
|
+
result.push({ type: "message", msg, key: `${msg.eventType}-${msg.index}` });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (logBuffer.length > 0) {
|
|
123
|
+
result.push({
|
|
124
|
+
type: "thinking-block",
|
|
125
|
+
items: logBuffer,
|
|
126
|
+
active: true,
|
|
127
|
+
collapsed: false,
|
|
128
|
+
key: `thinking-${startIdx}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
87
134
|
const AssistantChat = ({
|
|
88
135
|
conversation,
|
|
89
136
|
setConversation,
|
|
@@ -96,24 +143,63 @@ const AssistantChat = ({
|
|
|
96
143
|
}) => {
|
|
97
144
|
const { formatMessage } = useIntl();
|
|
98
145
|
const [inputValue, setInputValue] = useState("");
|
|
146
|
+
const [inputExpanded, setInputExpanded] = useState(false);
|
|
99
147
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
|
100
148
|
const messagesRef = useRef(null);
|
|
149
|
+
const messagesEndRef = useRef(null);
|
|
150
|
+
const inputRef = useRef(null);
|
|
101
151
|
const scrollToBottomAfterSendRef = useRef(false);
|
|
102
152
|
|
|
153
|
+
const syncTextareaHeight = useCallback(() => {
|
|
154
|
+
const el = inputRef.current;
|
|
155
|
+
if (!el) return;
|
|
156
|
+
|
|
157
|
+
const style = window.getComputedStyle(el);
|
|
158
|
+
const lh = parseFloat(style.lineHeight) || 20;
|
|
159
|
+
const padY = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
|
|
160
|
+
const maxHeight = lh * MAX_INPUT_LINES + padY;
|
|
161
|
+
|
|
162
|
+
// overflow:hidden means no scrollbar → stable width → reliable scrollHeight in one read.
|
|
163
|
+
// overflow:auto only once content exceeds the max line count, when height is already
|
|
164
|
+
// capped so the scrollbar cannot trigger further reflow.
|
|
165
|
+
el.style.overflowY = "hidden";
|
|
166
|
+
el.style.height = "auto";
|
|
167
|
+
const h = Math.min(el.scrollHeight, maxHeight);
|
|
168
|
+
el.style.height = `${h}px`;
|
|
169
|
+
el.style.overflowY = el.scrollHeight > maxHeight ? "auto" : "hidden";
|
|
170
|
+
|
|
171
|
+
setInputExpanded(h > lh + padY + 2);
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
useLayoutEffect(() => {
|
|
175
|
+
syncTextareaHeight();
|
|
176
|
+
}, [inputValue, syncTextareaHeight]);
|
|
177
|
+
|
|
103
178
|
useEffect(() => {
|
|
104
179
|
const ref = serverMessageHandlerRef;
|
|
105
180
|
if (!ref) return;
|
|
106
181
|
ref.current = (event, payload) => {
|
|
107
|
-
const { content, isError } = getContentFromPayload(event, payload);
|
|
182
|
+
const { content, isError, priority, source } = getContentFromPayload(event, payload);
|
|
108
183
|
if (event === "stream") {
|
|
109
|
-
const source = payload?.source ?? null;
|
|
110
184
|
requestAnimationFrame(() => {
|
|
111
|
-
setConversation((prev) => appendStreamChunk(prev, source, content));
|
|
185
|
+
setConversation((prev) => appendStreamChunk(prev, source, content, priority));
|
|
112
186
|
});
|
|
113
187
|
} else if (event === "log") {
|
|
114
188
|
setConversation((prev) => [
|
|
115
189
|
...prev,
|
|
116
|
-
{ eventType: "log", side: "agent", content: content || " " },
|
|
190
|
+
{ eventType: "log", side: "agent", content: content || " ", priority, source },
|
|
191
|
+
]);
|
|
192
|
+
} else if (event === "error" && Number(priority) === 2) {
|
|
193
|
+
setConversation((prev) => [
|
|
194
|
+
...prev,
|
|
195
|
+
{
|
|
196
|
+
eventType: "error",
|
|
197
|
+
side: "agent",
|
|
198
|
+
content: content || " ",
|
|
199
|
+
priority: 2,
|
|
200
|
+
source,
|
|
201
|
+
isError: true,
|
|
202
|
+
},
|
|
117
203
|
]);
|
|
118
204
|
} else {
|
|
119
205
|
setConversation((prev) => [
|
|
@@ -123,6 +209,8 @@ const AssistantChat = ({
|
|
|
123
209
|
side: "agent",
|
|
124
210
|
content: content || "Error",
|
|
125
211
|
isError: true,
|
|
212
|
+
priority,
|
|
213
|
+
source,
|
|
126
214
|
},
|
|
127
215
|
]);
|
|
128
216
|
}
|
|
@@ -138,9 +226,20 @@ const AssistantChat = ({
|
|
|
138
226
|
[conversation]
|
|
139
227
|
);
|
|
140
228
|
|
|
229
|
+
const renderItems = useMemo(
|
|
230
|
+
() => buildRenderItems(visibleMessages),
|
|
231
|
+
[visibleMessages]
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const canSendMessages = useMemo(
|
|
235
|
+
() => conversation.some((m) => m.side === "agent"),
|
|
236
|
+
[conversation]
|
|
237
|
+
);
|
|
238
|
+
|
|
141
239
|
const handleSubmit = useCallback(
|
|
142
240
|
(e) => {
|
|
143
241
|
e?.preventDefault?.();
|
|
242
|
+
if (!canSendMessages) return;
|
|
144
243
|
const text = inputValue.trim();
|
|
145
244
|
if (!text) return;
|
|
146
245
|
scrollToBottomAfterSendRef.current = true;
|
|
@@ -153,17 +252,18 @@ const AssistantChat = ({
|
|
|
153
252
|
onSendMessage(text);
|
|
154
253
|
}
|
|
155
254
|
},
|
|
156
|
-
[inputValue, onSendMessage]
|
|
255
|
+
[inputValue, onSendMessage, canSendMessages]
|
|
157
256
|
);
|
|
158
257
|
|
|
159
258
|
const handleKeyDown = useCallback(
|
|
160
259
|
(e) => {
|
|
161
260
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
162
261
|
e.preventDefault();
|
|
262
|
+
if (!canSendMessages) return;
|
|
163
263
|
handleSubmit();
|
|
164
264
|
}
|
|
165
265
|
},
|
|
166
|
-
[handleSubmit]
|
|
266
|
+
[handleSubmit, canSendMessages]
|
|
167
267
|
);
|
|
168
268
|
|
|
169
269
|
const handleNewChat = useCallback(() => {
|
|
@@ -178,28 +278,63 @@ const AssistantChat = ({
|
|
|
178
278
|
}
|
|
179
279
|
}, []);
|
|
180
280
|
|
|
181
|
-
const
|
|
281
|
+
const checkScrollPositionFallback = useCallback(() => {
|
|
182
282
|
const el = messagesRef.current;
|
|
183
283
|
if (!el) return;
|
|
284
|
+
const { scrollHeight, scrollTop, clientHeight } = el;
|
|
285
|
+
if (clientHeight < 2) {
|
|
286
|
+
setShowScrollToBottom(false);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const hasOverflow = scrollHeight > clientHeight + SCROLL_OVERFLOW_EPSILON;
|
|
290
|
+
if (!hasOverflow) {
|
|
291
|
+
setShowScrollToBottom(false);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
184
294
|
const atBottom =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
setShowScrollToBottom((prev) => (prev !== !atBottom ? !atBottom : prev));
|
|
295
|
+
scrollHeight - scrollTop - clientHeight <= SCROLL_AT_BOTTOM_THRESHOLD;
|
|
296
|
+
setShowScrollToBottom(!atBottom);
|
|
188
297
|
}, []);
|
|
189
298
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
299
|
+
// Scroll-to-bottom button: IntersectionObserver on a bottom sentinel is reliable on first
|
|
300
|
+
// paint (pre-loaded chats); scrollHeight vs clientHeight can mis-report before layout settles.
|
|
301
|
+
useLayoutEffect(() => {
|
|
302
|
+
const root = messagesRef.current;
|
|
303
|
+
const sentinel = messagesEndRef.current;
|
|
304
|
+
if (!root || !sentinel) return undefined;
|
|
305
|
+
|
|
306
|
+
if (typeof IntersectionObserver !== "function") {
|
|
307
|
+
checkScrollPositionFallback();
|
|
308
|
+
const rafId = requestAnimationFrame(() => {
|
|
309
|
+
checkScrollPositionFallback();
|
|
198
310
|
});
|
|
311
|
+
root.addEventListener("scroll", checkScrollPositionFallback);
|
|
312
|
+
return () => {
|
|
313
|
+
cancelAnimationFrame(rafId);
|
|
314
|
+
root.removeEventListener("scroll", checkScrollPositionFallback);
|
|
315
|
+
};
|
|
199
316
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
317
|
+
|
|
318
|
+
const io = new IntersectionObserver(
|
|
319
|
+
([entry]) => {
|
|
320
|
+
if (!entry) return;
|
|
321
|
+
setShowScrollToBottom(!entry.isIntersecting);
|
|
322
|
+
},
|
|
323
|
+
{ root, threshold: 0 }
|
|
324
|
+
);
|
|
325
|
+
io.observe(sentinel);
|
|
326
|
+
return () => io.disconnect();
|
|
327
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one observer; DOM/layout updates re-fire intersection
|
|
328
|
+
}, []);
|
|
329
|
+
|
|
330
|
+
useLayoutEffect(() => {
|
|
331
|
+
const el = messagesRef.current;
|
|
332
|
+
if (!el || !scrollToBottomAfterSendRef.current) return;
|
|
333
|
+
scrollToBottomAfterSendRef.current = false;
|
|
334
|
+
requestAnimationFrame(() => {
|
|
335
|
+
el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
|
|
336
|
+
});
|
|
337
|
+
}, [visibleMessages.length]);
|
|
203
338
|
|
|
204
339
|
const className = [
|
|
205
340
|
"assistant-chat",
|
|
@@ -237,21 +372,60 @@ const AssistantChat = ({
|
|
|
237
372
|
role="log"
|
|
238
373
|
aria-label={formatMessage({ id: "assistant.messages.history" })}
|
|
239
374
|
>
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
375
|
+
{renderItems.map((item) => {
|
|
376
|
+
if (item.type === "thinking-block") {
|
|
377
|
+
return (
|
|
378
|
+
<ThinkingOutLoud
|
|
379
|
+
key={item.key}
|
|
380
|
+
items={item.items}
|
|
381
|
+
active={item.active}
|
|
382
|
+
collapsed={item.collapsed}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const { msg } = item;
|
|
387
|
+
const isP0 = msg.priority === 0;
|
|
388
|
+
const isErrorP0 = msg.isError && isP0;
|
|
389
|
+
const sourceLabel = isP0 && msg.source ? getSourceLabel(msg.source) : null;
|
|
390
|
+
return (
|
|
391
|
+
<div
|
|
392
|
+
key={item.key}
|
|
393
|
+
className={[
|
|
394
|
+
"assistant-chat__message",
|
|
395
|
+
`assistant-chat__message--${msg.eventType}`,
|
|
396
|
+
`assistant-chat__message--${msg.side}`,
|
|
397
|
+
isErrorP0 && "assistant-chat__message--error",
|
|
398
|
+
].filter(Boolean).join(" ")}
|
|
399
|
+
>
|
|
400
|
+
{sourceLabel && (
|
|
401
|
+
<div className="assistant-chat__message-source">{sourceLabel}</div>
|
|
250
402
|
)}
|
|
403
|
+
<div className="assistant-chat__message-content">
|
|
404
|
+
{msg.content?.trim() ? (
|
|
405
|
+
<MarkdownReader content={msg.content} />
|
|
406
|
+
) : (
|
|
407
|
+
msg.content
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
251
410
|
</div>
|
|
252
|
-
|
|
253
|
-
)
|
|
411
|
+
);
|
|
412
|
+
})}
|
|
413
|
+
<div
|
|
414
|
+
ref={messagesEndRef}
|
|
415
|
+
className="assistant-chat__messages-end"
|
|
416
|
+
aria-hidden
|
|
417
|
+
/>
|
|
254
418
|
</div>
|
|
419
|
+
{!canSendMessages && (
|
|
420
|
+
<div
|
|
421
|
+
className="assistant-chat__waiting"
|
|
422
|
+
role="status"
|
|
423
|
+
aria-live="polite"
|
|
424
|
+
aria-label={formatMessage({ id: "assistant.chat.loading" })}
|
|
425
|
+
>
|
|
426
|
+
<span className="assistant-chat__waiting-spinner" aria-hidden />
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
255
429
|
{showScrollToBottom && (
|
|
256
430
|
<button
|
|
257
431
|
type="button"
|
|
@@ -264,22 +438,33 @@ const AssistantChat = ({
|
|
|
264
438
|
)}
|
|
265
439
|
</div>
|
|
266
440
|
<div className="assistant-chat__footer">
|
|
267
|
-
<form
|
|
268
|
-
|
|
269
|
-
|
|
441
|
+
<form
|
|
442
|
+
className={[
|
|
443
|
+
"assistant-chat__input-row",
|
|
444
|
+
inputExpanded && "assistant-chat__input-row--expanded",
|
|
445
|
+
]
|
|
446
|
+
.filter(Boolean)
|
|
447
|
+
.join(" ")}
|
|
448
|
+
onSubmit={handleSubmit}
|
|
449
|
+
>
|
|
450
|
+
<textarea
|
|
451
|
+
ref={inputRef}
|
|
452
|
+
rows={1}
|
|
270
453
|
className="assistant-chat__input"
|
|
271
454
|
placeholder={formatMessage({ id: "assistant.input.placeholder" })}
|
|
272
455
|
value={inputValue}
|
|
273
456
|
onChange={(e) => setInputValue(e.target.value)}
|
|
274
457
|
onKeyDown={handleKeyDown}
|
|
275
458
|
aria-label={formatMessage({ id: "assistant.input.label" })}
|
|
459
|
+
disabled={!canSendMessages}
|
|
276
460
|
/>
|
|
277
461
|
<button
|
|
278
462
|
type="submit"
|
|
279
463
|
className="assistant-chat__send"
|
|
280
464
|
aria-label={formatMessage({ id: "assistant.actions.send" })}
|
|
465
|
+
disabled={!canSendMessages}
|
|
281
466
|
>
|
|
282
|
-
<Icon name="
|
|
467
|
+
<Icon name="arrow up" />
|
|
283
468
|
</button>
|
|
284
469
|
</form>
|
|
285
470
|
<div className="assistant-chat__disclaimer">
|
|
@@ -24,4 +24,83 @@ describe("<AssistantChat />", () => {
|
|
|
24
24
|
await waitForLoad(rendered);
|
|
25
25
|
expect(rendered.container).toMatchSnapshot();
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
it("groups logs in a thinking block and auto-collapses when a priority-0 response arrives", async () => {
|
|
29
|
+
const conversation = [
|
|
30
|
+
{
|
|
31
|
+
eventType: "log",
|
|
32
|
+
side: "agent",
|
|
33
|
+
content: "Workflow started",
|
|
34
|
+
priority: 1,
|
|
35
|
+
source: "workflow_normalizer",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
eventType: "log",
|
|
39
|
+
side: "agent",
|
|
40
|
+
content: "Calling tool",
|
|
41
|
+
priority: 2,
|
|
42
|
+
source: "workflow_normalizer.agent_normalize_10015c7d",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
eventType: "response",
|
|
46
|
+
side: "agent",
|
|
47
|
+
content: "Final answer",
|
|
48
|
+
priority: 0,
|
|
49
|
+
source: "welcome",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const rendered = render(
|
|
54
|
+
<AssistantChat {...defaultProps} conversation={conversation} />
|
|
55
|
+
);
|
|
56
|
+
await waitForLoad(rendered);
|
|
57
|
+
|
|
58
|
+
expect(rendered.container.querySelector(".thinking-block")).toBeInTheDocument();
|
|
59
|
+
expect(rendered.container.querySelector(".thinking-section__body")).not.toBeInTheDocument();
|
|
60
|
+
expect(rendered.container.querySelector(".thinking-section__header")).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("shows source label for priority-0 response messages", async () => {
|
|
64
|
+
const conversation = [
|
|
65
|
+
{
|
|
66
|
+
eventType: "response",
|
|
67
|
+
side: "agent",
|
|
68
|
+
content: "Final answer",
|
|
69
|
+
priority: 0,
|
|
70
|
+
source: "workflow_normalizer_ff83ae63.agent_normalize_10015c7d",
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const rendered = render(
|
|
75
|
+
<AssistantChat {...defaultProps} conversation={conversation} />
|
|
76
|
+
);
|
|
77
|
+
await waitForLoad(rendered);
|
|
78
|
+
|
|
79
|
+
expect(rendered.getByText("Agent Normalize")).toBeInTheDocument();
|
|
80
|
+
expect(rendered.getByText("Final answer")).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("renders priority-0 errors with error style and source label", async () => {
|
|
84
|
+
const conversation = [
|
|
85
|
+
{
|
|
86
|
+
eventType: "response",
|
|
87
|
+
side: "agent",
|
|
88
|
+
content: "Something failed",
|
|
89
|
+
isError: true,
|
|
90
|
+
priority: 0,
|
|
91
|
+
source: "workflow_normalizer",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const rendered = render(
|
|
96
|
+
<AssistantChat {...defaultProps} conversation={conversation} />
|
|
97
|
+
);
|
|
98
|
+
await waitForLoad(rendered);
|
|
99
|
+
|
|
100
|
+
const errorMessage = rendered.container.querySelector(
|
|
101
|
+
".assistant-chat__message--error"
|
|
102
|
+
);
|
|
103
|
+
expect(errorMessage).toBeInTheDocument();
|
|
104
|
+
expect(rendered.getByText("Workflow Normalizer")).toBeInTheDocument();
|
|
105
|
+
});
|
|
27
106
|
});
|
|
@@ -39,22 +39,45 @@ exports[`<AssistantChat /> matches the latest snapshot 1`] = `
|
|
|
39
39
|
role="log"
|
|
40
40
|
>
|
|
41
41
|
<div
|
|
42
|
-
class="
|
|
42
|
+
class="thinking-block"
|
|
43
43
|
>
|
|
44
44
|
<div
|
|
45
|
-
class="
|
|
45
|
+
class="thinking-section"
|
|
46
46
|
>
|
|
47
|
+
<button
|
|
48
|
+
class="thinking-section__header"
|
|
49
|
+
type="button"
|
|
50
|
+
>
|
|
51
|
+
<i
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
class="chevron down icon"
|
|
54
|
+
/>
|
|
55
|
+
<span
|
|
56
|
+
class="thinking-section__label"
|
|
57
|
+
/>
|
|
58
|
+
<span
|
|
59
|
+
class="thinking-section__spinner"
|
|
60
|
+
>
|
|
61
|
+
<span
|
|
62
|
+
class="thinking-section__spinner-dot"
|
|
63
|
+
/>
|
|
64
|
+
</span>
|
|
65
|
+
</button>
|
|
47
66
|
<div
|
|
48
|
-
class="
|
|
67
|
+
class="thinking-section__body"
|
|
49
68
|
>
|
|
50
|
-
<
|
|
69
|
+
<div
|
|
70
|
+
class="thinking-section__item thinking-section__item--1"
|
|
71
|
+
>
|
|
51
72
|
Requesting connection
|
|
52
|
-
</
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
</div>
|
|
55
74
|
</div>
|
|
56
75
|
</div>
|
|
57
76
|
</div>
|
|
77
|
+
<div
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
class="assistant-chat__messages-end"
|
|
80
|
+
/>
|
|
58
81
|
</div>
|
|
59
82
|
</div>
|
|
60
83
|
<div
|
|
@@ -63,12 +86,12 @@ exports[`<AssistantChat /> matches the latest snapshot 1`] = `
|
|
|
63
86
|
<form
|
|
64
87
|
class="assistant-chat__input-row"
|
|
65
88
|
>
|
|
66
|
-
<
|
|
89
|
+
<textarea
|
|
67
90
|
aria-label="assistant.input.label"
|
|
68
91
|
class="assistant-chat__input"
|
|
69
92
|
placeholder="assistant.input.placeholder"
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
rows="1"
|
|
94
|
+
style="overflow-y: hidden; height: 0px;"
|
|
72
95
|
/>
|
|
73
96
|
<button
|
|
74
97
|
aria-label="assistant.actions.send"
|
|
@@ -77,7 +100,7 @@ exports[`<AssistantChat /> matches the latest snapshot 1`] = `
|
|
|
77
100
|
>
|
|
78
101
|
<i
|
|
79
102
|
aria-hidden="true"
|
|
80
|
-
class="
|
|
103
|
+
class="arrow up icon"
|
|
81
104
|
/>
|
|
82
105
|
</button>
|
|
83
106
|
</form>
|
package/src/components/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import AiRoutes from "./AiRoutes";
|
|
2
2
|
import TranslationModal from "./TranslationModal";
|
|
3
3
|
import Assistant from "./assistant/Assistant";
|
|
4
|
-
|
|
4
|
+
import ThinkingOutLoud, { getSourceLabel } from "./ThinkingOutLoud";
|
|
5
|
+
export { AiRoutes, TranslationModal, Assistant, ThinkingOutLoud, getSourceLabel };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import { FormattedMessage } from "react-intl";
|
|
3
|
+
import { Popup } from "semantic-ui-react";
|
|
4
|
+
|
|
5
|
+
const similarityColumnDecorator = (similarity) => {
|
|
6
|
+
if (similarity == null || Number.isNaN(Number(similarity))) {
|
|
7
|
+
return <span>—</span>;
|
|
8
|
+
}
|
|
9
|
+
const n = Number(similarity);
|
|
10
|
+
return (
|
|
11
|
+
<Popup
|
|
12
|
+
content={<FormattedMessage id="suggestions.similarity.cosine.popup" />}
|
|
13
|
+
trigger={<span>{n.toFixed(3)}</span>}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const reasonColumnDecorator = (reason) => {
|
|
19
|
+
if (reason == null || reason === "") {
|
|
20
|
+
return <span>—</span>;
|
|
21
|
+
}
|
|
22
|
+
return (
|
|
23
|
+
<span
|
|
24
|
+
style={{ whiteSpace: "normal", wordBreak: "break-word" }}
|
|
25
|
+
title={String(reason)}
|
|
26
|
+
>
|
|
27
|
+
{String(reason)}
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const similarityColumnDefinition = {
|
|
33
|
+
name: "similarity",
|
|
34
|
+
fieldSelector: _.prop("similarity"),
|
|
35
|
+
fieldDecorator: similarityColumnDecorator,
|
|
36
|
+
width: 1,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const reasonColumnDefinition = {
|
|
40
|
+
name: "reason",
|
|
41
|
+
fieldSelector: _.prop("reason"),
|
|
42
|
+
fieldDecorator: reasonColumnDecorator,
|
|
43
|
+
width: 3,
|
|
44
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import useSWRMutations from "swr/mutation";
|
|
2
|
+
import { apiJsonPost } from "@truedat/core/services/api";
|
|
3
|
+
import { API_AGENT_LAYER_RUN } from "../api";
|
|
4
|
+
|
|
5
|
+
export const useAgentLayerRun = () => {
|
|
6
|
+
return useSWRMutations(API_AGENT_LAYER_RUN, (url, { arg }) =>
|
|
7
|
+
apiJsonPost(url, arg)
|
|
8
|
+
);
|
|
9
|
+
};
|