@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.
@@ -1,33 +1,32 @@
1
- import { useState, useCallback, useMemo, useRef, useEffect } from "react";
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
- let lastRequestOrResponseIndex = -1;
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
- if (!payload || typeof payload !== "object") return { content: "", isError: false };
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 checkScrollPosition = useCallback(() => {
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
- el.scrollHeight - el.scrollTop - el.clientHeight <=
186
- SCROLL_AT_BOTTOM_THRESHOLD;
187
- setShowScrollToBottom((prev) => (prev !== !atBottom ? !atBottom : prev));
295
+ scrollHeight - scrollTop - clientHeight <= SCROLL_AT_BOTTOM_THRESHOLD;
296
+ setShowScrollToBottom(!atBottom);
188
297
  }, []);
189
298
 
190
- useEffect(() => {
191
- const el = messagesRef.current;
192
- if (!el) return;
193
- checkScrollPosition();
194
- if (scrollToBottomAfterSendRef.current) {
195
- scrollToBottomAfterSendRef.current = false;
196
- requestAnimationFrame(() => {
197
- el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
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
- el.addEventListener("scroll", checkScrollPosition);
201
- return () => el.removeEventListener("scroll", checkScrollPosition);
202
- }, [checkScrollPosition, visibleMessages.length]);
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
- {visibleMessages.map((msg) => (
241
- <div
242
- key={`${msg.eventType}-${msg.index}`}
243
- className={`assistant-chat__message assistant-chat__message--${msg.eventType} assistant-chat__message--${msg.side}`}
244
- >
245
- <div className="assistant-chat__message-content">
246
- {msg.side === "agent" && msg.content?.trim() ? (
247
- <MarkdownReader content={msg.content} />
248
- ) : (
249
- msg.content
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
- </div>
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 className="assistant-chat__input-row" onSubmit={handleSubmit}>
268
- <input
269
- type="text"
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="send" />
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="assistant-chat__message assistant-chat__message--log assistant-chat__message--agent"
42
+ class="thinking-block"
43
43
  >
44
44
  <div
45
- class="assistant-chat__message-content"
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="markdown-reader"
67
+ class="thinking-section__body"
49
68
  >
50
- <p>
69
+ <div
70
+ class="thinking-section__item thinking-section__item--1"
71
+ >
51
72
  Requesting connection
52
- </p>
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
- <input
89
+ <textarea
67
90
  aria-label="assistant.input.label"
68
91
  class="assistant-chat__input"
69
92
  placeholder="assistant.input.placeholder"
70
- type="text"
71
- value=""
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="send icon"
103
+ class="arrow up icon"
81
104
  />
82
105
  </button>
83
106
  </form>
@@ -1,4 +1,5 @@
1
1
  import AiRoutes from "./AiRoutes";
2
2
  import TranslationModal from "./TranslationModal";
3
3
  import Assistant from "./assistant/Assistant";
4
- export { AiRoutes, TranslationModal, Assistant };
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
+ };