@truedat/ai 8.8.7 → 8.9.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/package.json +3 -3
- package/src/api.js +0 -5
- package/src/components/suggestions/SuggestionsWidget.js +235 -57
- package/src/components/suggestions/__tests__/SuggestionsWidget.spec.js +383 -94
- package/src/hooks/__mocks__/useSuggestions.js +1 -1
- package/src/hooks/useSuggestions.js +14 -6
- package/src/components/suggestions/__tests__/__snapshots__/SuggestionsWidget.spec.js.snap +0 -313
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/ai",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.9.0",
|
|
4
4
|
"description": "Truedat Web Artificial Intelligence package",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@testing-library/jest-dom": "^6.6.3",
|
|
51
51
|
"@testing-library/react": "^16.3.0",
|
|
52
52
|
"@testing-library/user-event": "^14.6.1",
|
|
53
|
-
"@truedat/test": "8.
|
|
53
|
+
"@truedat/test": "8.9.0",
|
|
54
54
|
"identity-obj-proxy": "^3.0.0",
|
|
55
55
|
"jest": "^29.7.0",
|
|
56
56
|
"redux-saga-test-plan": "^4.0.6"
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"semantic-ui-react": "^3.0.0-beta.2",
|
|
81
81
|
"swr": "^2.3.3"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "beff9d7e01556d63d42e54c9f7b641503ee658b2"
|
|
84
84
|
}
|
package/src/api.js
CHANGED
|
@@ -10,9 +10,6 @@ const API_PROMPT_SET_ACTIVE = "/api/prompts/:id/set_active";
|
|
|
10
10
|
const API_PROVIDERS = "/api/providers";
|
|
11
11
|
const API_PROVIDER = "/api/providers/:id";
|
|
12
12
|
const API_PROVIDER_CHAT = "/api/providers/:id/chat_completion";
|
|
13
|
-
const API_SUGGESTIONS_AVAILABILITY_CHECK =
|
|
14
|
-
"/api/suggestions/availability_check";
|
|
15
|
-
const API_SUGGESTIONS_REQUEST = "/api/suggestions/request";
|
|
16
13
|
const API_TRANSLATIONS_AVAILABILITY_CHECK =
|
|
17
14
|
"/api/translations/availability_check";
|
|
18
15
|
const API_TRANSLATIONS_REQUEST = "/api/translations/request";
|
|
@@ -35,8 +32,6 @@ export {
|
|
|
35
32
|
API_PROVIDERS,
|
|
36
33
|
API_PROVIDER,
|
|
37
34
|
API_PROVIDER_CHAT,
|
|
38
|
-
API_SUGGESTIONS_AVAILABILITY_CHECK,
|
|
39
|
-
API_SUGGESTIONS_REQUEST,
|
|
40
35
|
API_TRANSLATIONS_AVAILABILITY_CHECK,
|
|
41
36
|
API_TRANSLATIONS_REQUEST,
|
|
42
37
|
API_AGENT_LAYER_CONVERSATION,
|
|
@@ -1,31 +1,98 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
6
6
|
Checkbox,
|
|
7
7
|
Container,
|
|
8
8
|
Divider,
|
|
9
|
+
Form,
|
|
9
10
|
Icon,
|
|
10
11
|
List,
|
|
11
12
|
Message,
|
|
12
13
|
Segment,
|
|
13
14
|
Popup,
|
|
14
15
|
} from "semantic-ui-react";
|
|
15
|
-
import { FormattedMessage } from "react-intl";
|
|
16
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
17
|
+
import { MarkdownReader } from "@truedat/core/components";
|
|
18
|
+
import { useWebContext } from "@truedat/core/webContext";
|
|
19
|
+
import { useAgentLayerRun } from "../../hooks/useAgentLayerRun";
|
|
20
|
+
import { useAvailabilityCheck } from "../../hooks/useSuggestions";
|
|
21
|
+
import useAssistantSocket from "../assistant/hooks/useAssistantSocket";
|
|
22
|
+
import ThinkingOutLoud from "../ThinkingOutLoud";
|
|
23
|
+
|
|
24
|
+
const parseStreamResult = (content) => {
|
|
25
|
+
try {
|
|
26
|
+
const raw = typeof content === "string" ? JSON.parse(content) : content;
|
|
27
|
+
return raw?.result ?? raw ?? {};
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const formatLogContent = (log) =>
|
|
34
|
+
log && typeof log === "object"
|
|
35
|
+
? (log.message ?? log.level ?? JSON.stringify(log))
|
|
36
|
+
: String(log ?? "");
|
|
37
|
+
|
|
38
|
+
const formatErrorContent = (err) =>
|
|
39
|
+
err && typeof err === "object"
|
|
40
|
+
? (err.message ?? err.reason ?? JSON.stringify(err))
|
|
41
|
+
: String(err ?? "");
|
|
16
42
|
|
|
17
43
|
export default function SuggestionsWidget({
|
|
18
|
-
|
|
44
|
+
suggestionParams,
|
|
19
45
|
applySuggestions,
|
|
20
46
|
template,
|
|
21
47
|
isModification,
|
|
22
48
|
enableRequestButton = true,
|
|
23
49
|
disabledButtonMessage = "",
|
|
24
50
|
}) {
|
|
25
|
-
const
|
|
51
|
+
const { formatMessage } = useIntl();
|
|
52
|
+
|
|
53
|
+
const [requested, setRequested] = useState(false);
|
|
54
|
+
const [promptPopupOpen, setPromptPopupOpen] = useState(false);
|
|
55
|
+
const [prompt, setPrompt] = useState("");
|
|
26
56
|
const [suggestions, setSuggestions] = useState();
|
|
27
57
|
const [suggestionsError, setSuggestionsError] = useState();
|
|
28
58
|
const [selectedSuggestions, setSelectedSuggestions] = useState([]);
|
|
59
|
+
const [logItems, setLogItems] = useState([]);
|
|
60
|
+
|
|
61
|
+
const { disable_td_ai } = useWebContext();
|
|
62
|
+
const { trigger: checkAvailability } = useAvailabilityCheck();
|
|
63
|
+
const availabilityRequestId = useRef(0);
|
|
64
|
+
const [available, setAvailable] = useState(false);
|
|
65
|
+
|
|
66
|
+
const availabilityTemplateId = suggestionParams?.template_id;
|
|
67
|
+
const availabilityResourceType = suggestionParams?.resource_type;
|
|
68
|
+
const availabilityDomainIds = suggestionParams?.domain_ids;
|
|
69
|
+
const availabilityDomainKey = JSON.stringify(availabilityDomainIds ?? []);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (disable_td_ai || !availabilityTemplateId) {
|
|
73
|
+
setAvailable(false);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Only apply the latest response; params can change while a check is pending.
|
|
77
|
+
availabilityRequestId.current += 1;
|
|
78
|
+
const requestId = availabilityRequestId.current;
|
|
79
|
+
setAvailable(false);
|
|
80
|
+
checkAvailability({
|
|
81
|
+
template_id: availabilityTemplateId,
|
|
82
|
+
domain_ids: availabilityDomainIds || [],
|
|
83
|
+
resource_type: availabilityResourceType,
|
|
84
|
+
}).then((res) => {
|
|
85
|
+
if (availabilityRequestId.current === requestId) {
|
|
86
|
+
setAvailable(res.available);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
+
}, [
|
|
91
|
+
disable_td_ai,
|
|
92
|
+
availabilityTemplateId,
|
|
93
|
+
availabilityResourceType,
|
|
94
|
+
availabilityDomainKey,
|
|
95
|
+
]);
|
|
29
96
|
|
|
30
97
|
const suggestionKeys = _.keys(suggestions);
|
|
31
98
|
const fieldInSuggestions = ({ name }) => _.includes(name)(suggestionKeys);
|
|
@@ -49,33 +116,82 @@ export default function SuggestionsWidget({
|
|
|
49
116
|
)(template);
|
|
50
117
|
|
|
51
118
|
const validFields = _.keys(fieldTypes);
|
|
119
|
+
// Keeps handleServerMessage's identity stable (see below) while still
|
|
120
|
+
// reading the latest validFields when a stream result arrives.
|
|
121
|
+
const validFieldsRef = useRef(validFields);
|
|
122
|
+
validFieldsRef.current = validFields;
|
|
52
123
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
124
|
+
const { trigger: fetchSuggestions, data: runData } = useAgentLayerRun();
|
|
125
|
+
const agentStateId = runData?.data?.data?.agent_state_id
|
|
126
|
+
? String(runData.data.data.agent_state_id)
|
|
127
|
+
: null;
|
|
128
|
+
|
|
129
|
+
const handleServerMessage = useCallback((event, payload) => {
|
|
130
|
+
const priority = payload?.priority ?? null;
|
|
58
131
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
} else {
|
|
66
|
-
setSuggestions(suggestions);
|
|
67
|
-
const selectedFields = _.flow(
|
|
132
|
+
if (event === "stream" && Number(priority) === 0) {
|
|
133
|
+
const parsed = parseStreamResult(payload?.content);
|
|
134
|
+
setSuggestions(parsed);
|
|
135
|
+
setSelectedSuggestions(
|
|
136
|
+
_.flow(
|
|
68
137
|
_.keys,
|
|
69
138
|
_.filter(
|
|
70
139
|
(key) =>
|
|
71
|
-
_.includes(key)(
|
|
140
|
+
_.includes(key)(validFieldsRef.current) &&
|
|
141
|
+
!_.isEmpty(parsed[key]),
|
|
72
142
|
),
|
|
73
|
-
)(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
143
|
+
)(parsed),
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (event === "error" && Number(priority) !== 2) {
|
|
149
|
+
setSuggestionsError(formatErrorContent(payload?.error));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (event === "log" || (event === "error" && Number(priority) === 2)) {
|
|
154
|
+
const source = payload?.source ?? null;
|
|
155
|
+
const content =
|
|
156
|
+
event === "log"
|
|
157
|
+
? formatLogContent(payload?.content)
|
|
158
|
+
: formatErrorContent(payload?.error);
|
|
159
|
+
|
|
160
|
+
setLogItems((prev) => [
|
|
161
|
+
...prev,
|
|
162
|
+
{ eventType: event, content: content || " ", priority, source },
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
useAssistantSocket(!agentStateId, {
|
|
168
|
+
conversationId: agentStateId,
|
|
169
|
+
onServerMessage: handleServerMessage,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const inProgress = requested && !suggestions && !suggestionsError;
|
|
173
|
+
const requestDisabled = !enableRequestButton || inProgress;
|
|
174
|
+
|
|
175
|
+
const resetState = () => {
|
|
176
|
+
setRequested(false);
|
|
177
|
+
setSuggestions(null);
|
|
178
|
+
setSuggestionsError(null);
|
|
179
|
+
setSelectedSuggestions([]);
|
|
180
|
+
setLogItems([]);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleRequestSuggestions = (requestPrompt) => {
|
|
184
|
+
setSuggestionsError(null);
|
|
185
|
+
setSelectedSuggestions([]);
|
|
186
|
+
setSuggestions(null);
|
|
187
|
+
setLogItems([]);
|
|
188
|
+
setRequested(true);
|
|
189
|
+
fetchSuggestions({
|
|
190
|
+
workflow: "suggest_content",
|
|
191
|
+
params: { ...suggestionParams, prompt: requestPrompt },
|
|
77
192
|
});
|
|
78
193
|
};
|
|
194
|
+
|
|
79
195
|
const handleApplySuggestions = () => {
|
|
80
196
|
_.flow(
|
|
81
197
|
_.pick(selectedSuggestions),
|
|
@@ -91,8 +207,7 @@ export default function SuggestionsWidget({
|
|
|
91
207
|
applySuggestions,
|
|
92
208
|
)(suggestions);
|
|
93
209
|
|
|
94
|
-
|
|
95
|
-
setSuggestions(null);
|
|
210
|
+
resetState();
|
|
96
211
|
};
|
|
97
212
|
|
|
98
213
|
const toggleSelectedSuggestion = (name) => {
|
|
@@ -103,8 +218,66 @@ export default function SuggestionsWidget({
|
|
|
103
218
|
);
|
|
104
219
|
};
|
|
105
220
|
|
|
221
|
+
const renderRequestControl = () => {
|
|
222
|
+
if (!enableRequestButton && disabledButtonMessage) {
|
|
223
|
+
return (
|
|
224
|
+
<Popup
|
|
225
|
+
content={disabledButtonMessage}
|
|
226
|
+
trigger={
|
|
227
|
+
<span>
|
|
228
|
+
<Button disabled>
|
|
229
|
+
<FormattedMessage id="actions.ai_suggestion" />
|
|
230
|
+
</Button>
|
|
231
|
+
</span>
|
|
232
|
+
}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return (
|
|
237
|
+
<Popup
|
|
238
|
+
on="click"
|
|
239
|
+
open={promptPopupOpen}
|
|
240
|
+
onOpen={() => setPromptPopupOpen(true)}
|
|
241
|
+
onClose={() => setPromptPopupOpen(false)}
|
|
242
|
+
disabled={requestDisabled}
|
|
243
|
+
content={
|
|
244
|
+
<Form style={{ minWidth: "300px" }}>
|
|
245
|
+
<Form.Field>
|
|
246
|
+
<label>{formatMessage({ id: "suggestions.prompt.label" })}</label>
|
|
247
|
+
<Form.Input
|
|
248
|
+
value={prompt}
|
|
249
|
+
onChange={(_e, { value }) => setPrompt(value)}
|
|
250
|
+
placeholder={formatMessage({
|
|
251
|
+
id: "suggestions.prompt.placeholder",
|
|
252
|
+
})}
|
|
253
|
+
/>
|
|
254
|
+
</Form.Field>
|
|
255
|
+
<Button
|
|
256
|
+
primary
|
|
257
|
+
content={formatMessage({ id: "suggestions.prompt.submit" })}
|
|
258
|
+
onClick={() => {
|
|
259
|
+
setPromptPopupOpen(false);
|
|
260
|
+
handleRequestSuggestions(prompt);
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
</Form>
|
|
264
|
+
}
|
|
265
|
+
position="bottom center"
|
|
266
|
+
trigger={
|
|
267
|
+
<span>
|
|
268
|
+
<Button disabled={requestDisabled}>
|
|
269
|
+
<FormattedMessage id="actions.ai_suggestion" />
|
|
270
|
+
</Button>
|
|
271
|
+
</span>
|
|
272
|
+
}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (!available) return null;
|
|
278
|
+
|
|
106
279
|
return (
|
|
107
|
-
<Segment
|
|
280
|
+
<Segment>
|
|
108
281
|
<Container style={{ display: "flex", justifyContent: "space-between" }}>
|
|
109
282
|
<h4>
|
|
110
283
|
{suggestions ? (
|
|
@@ -115,33 +288,27 @@ export default function SuggestionsWidget({
|
|
|
115
288
|
<FormattedMessage id="structure_note.ai_suggestion.header" />
|
|
116
289
|
</h4>
|
|
117
290
|
<Divider hidden />
|
|
118
|
-
{
|
|
119
|
-
<Popup
|
|
120
|
-
content={disabledButtonMessage}
|
|
121
|
-
trigger={
|
|
122
|
-
<span>
|
|
123
|
-
<Button onClick={handleRequestSuggestions} disabled>
|
|
124
|
-
<FormattedMessage id="actions.ai_suggestion" />
|
|
125
|
-
</Button>
|
|
126
|
-
</span>
|
|
127
|
-
}
|
|
128
|
-
/>
|
|
129
|
-
) : (
|
|
130
|
-
<Button
|
|
131
|
-
onClick={handleRequestSuggestions}
|
|
132
|
-
disabled={!enableRequestButton}
|
|
133
|
-
>
|
|
134
|
-
<FormattedMessage id="actions.ai_suggestion" />
|
|
135
|
-
</Button>
|
|
136
|
-
)}
|
|
291
|
+
{renderRequestControl()}
|
|
137
292
|
</Container>
|
|
138
293
|
{suggestionsError ? (
|
|
139
|
-
|
|
140
|
-
<Message
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
294
|
+
<>
|
|
295
|
+
<Message negative>
|
|
296
|
+
<Message.Header>
|
|
297
|
+
<FormattedMessage id="structure_note.ai_suggestion.error" />
|
|
298
|
+
</Message.Header>
|
|
299
|
+
<p>{suggestionsError}</p>
|
|
300
|
+
</Message>
|
|
301
|
+
<Container textAlign="right">
|
|
302
|
+
<Button basic onClick={resetState}>
|
|
303
|
+
<FormattedMessage id="actions.discard_ai_suggestion" />
|
|
304
|
+
</Button>
|
|
305
|
+
</Container>
|
|
306
|
+
</>
|
|
307
|
+
) : null}
|
|
308
|
+
{inProgress ? (
|
|
309
|
+
<Segment loading={logItems.length === 0} basic>
|
|
310
|
+
<ThinkingOutLoud items={logItems} active />
|
|
311
|
+
</Segment>
|
|
145
312
|
) : null}
|
|
146
313
|
{suggestions ? (
|
|
147
314
|
<>
|
|
@@ -150,12 +317,12 @@ export default function SuggestionsWidget({
|
|
|
150
317
|
<>
|
|
151
318
|
<h5>{groupName ? groupName : ""}</h5>
|
|
152
319
|
<List>
|
|
153
|
-
{_.map(({ name, label, editable = true }) => (
|
|
320
|
+
{_.map(({ name, label, editable = true, widget }) => (
|
|
154
321
|
<List.Item
|
|
155
322
|
key={name}
|
|
156
323
|
style={{
|
|
157
324
|
display: "flex",
|
|
158
|
-
alignItems: "
|
|
325
|
+
alignItems: "flex-start",
|
|
159
326
|
gap: "12px",
|
|
160
327
|
}}
|
|
161
328
|
>
|
|
@@ -175,9 +342,13 @@ export default function SuggestionsWidget({
|
|
|
175
342
|
>
|
|
176
343
|
<List.Header>{label}</List.Header>
|
|
177
344
|
<List.Description>
|
|
178
|
-
{_.isEmpty(suggestions[name])
|
|
179
|
-
|
|
180
|
-
|
|
345
|
+
{_.isEmpty(suggestions[name]) ? (
|
|
346
|
+
"-"
|
|
347
|
+
) : widget === "markdown" ? (
|
|
348
|
+
<MarkdownReader content={suggestions[name]} />
|
|
349
|
+
) : (
|
|
350
|
+
suggestions[name]
|
|
351
|
+
)}
|
|
181
352
|
</List.Description>
|
|
182
353
|
</label>
|
|
183
354
|
</List.Item>
|
|
@@ -188,6 +359,13 @@ export default function SuggestionsWidget({
|
|
|
188
359
|
</Container>
|
|
189
360
|
))(templateFields)}
|
|
190
361
|
<Container textAlign="right">
|
|
362
|
+
<Button
|
|
363
|
+
className="td-icon-text-control"
|
|
364
|
+
secondary
|
|
365
|
+
onClick={resetState}
|
|
366
|
+
>
|
|
367
|
+
<FormattedMessage id="actions.discard_ai_suggestion" />
|
|
368
|
+
</Button>
|
|
191
369
|
<Button
|
|
192
370
|
primary
|
|
193
371
|
onClick={handleApplySuggestions}
|
|
@@ -203,7 +381,7 @@ export default function SuggestionsWidget({
|
|
|
203
381
|
}
|
|
204
382
|
|
|
205
383
|
SuggestionsWidget.propTypes = {
|
|
206
|
-
|
|
384
|
+
suggestionParams: PropTypes.object,
|
|
207
385
|
applySuggestions: PropTypes.func,
|
|
208
386
|
template: PropTypes.object,
|
|
209
387
|
isModification: PropTypes.bool,
|
|
@@ -1,153 +1,442 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { waitFor } from "@testing-library/react";
|
|
1
|
+
import { act, waitFor } from "@testing-library/react";
|
|
3
2
|
import userEvent from "@testing-library/user-event";
|
|
4
3
|
import { render } from "@truedat/test/render";
|
|
4
|
+
import useAssistantSocket from "../../assistant/hooks/useAssistantSocket";
|
|
5
|
+
import { useAgentLayerRun } from "../../../hooks/useAgentLayerRun";
|
|
6
|
+
import { useAvailabilityCheck } from "../../../hooks/useSuggestions";
|
|
5
7
|
import SuggestionsWidget from "../SuggestionsWidget";
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
9
|
+
jest.mock("../../assistant/hooks/useAssistantSocket", () => jest.fn());
|
|
10
|
+
|
|
11
|
+
jest.mock("../../../hooks/useAgentLayerRun", () => ({
|
|
12
|
+
useAgentLayerRun: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.mock("../../../hooks/useSuggestions", () => ({
|
|
16
|
+
useAvailabilityCheck: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const mockCheckAvailability = jest.fn();
|
|
12
20
|
|
|
13
21
|
const messages = {
|
|
14
22
|
en: {
|
|
15
|
-
"actions.cancel": "cancel",
|
|
16
|
-
"actions.save": "save",
|
|
17
|
-
"structure_note.ai_suggestion.header": "header",
|
|
18
23
|
"actions.apply_ai_suggestion": "actions.apply_ai_suggestion",
|
|
24
|
+
"actions.discard_ai_suggestion": "actions.discard_ai_suggestion",
|
|
19
25
|
"actions.ai_suggestion": "actions.ai_suggestion",
|
|
26
|
+
"structure_note.ai_suggestion.header": "header",
|
|
20
27
|
"structure_note.ai_suggestion.error": "error",
|
|
28
|
+
"suggestions.prompt.label": "suggestions.prompt.label",
|
|
29
|
+
"suggestions.prompt.placeholder": "suggestions.prompt.placeholder",
|
|
30
|
+
"suggestions.prompt.submit": "suggestions.prompt.submit",
|
|
21
31
|
},
|
|
22
32
|
};
|
|
23
33
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
: mock === "error_message"
|
|
31
|
-
? ["error", { error: { message: "ERROR MESSAGE" } }]
|
|
32
|
-
: ["error", "ERROR TEXT"],
|
|
34
|
+
const template = {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
fields: [
|
|
38
|
+
{ name: "editable_field", label: "EditableField", editable: true },
|
|
39
|
+
],
|
|
33
40
|
},
|
|
34
|
-
|
|
41
|
+
{
|
|
42
|
+
name: "Group2",
|
|
43
|
+
fields: [
|
|
44
|
+
{
|
|
45
|
+
name: "non_editable_field",
|
|
46
|
+
label: "NonEditableField",
|
|
47
|
+
editable: false,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "unselect_field",
|
|
51
|
+
label: "UnselectedField",
|
|
52
|
+
editable: true,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mockSuggestions = {
|
|
60
|
+
editable_field: "editable_field_value",
|
|
61
|
+
non_editable_field: "non_editable_field_value",
|
|
62
|
+
unselect_field: "unselect_field_value",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const user = userEvent.setup({ delay: null });
|
|
66
|
+
|
|
67
|
+
const requestSuggestions = async (findByText) => {
|
|
68
|
+
await user.click(await findByText(/actions.ai_suggestion/i));
|
|
69
|
+
await user.click(await findByText(/suggestions.prompt.submit/i));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Reads the onServerMessage callback the component most recently registered
|
|
73
|
+
// with the mocked useAssistantSocket, instead of capturing it via a `let`.
|
|
74
|
+
const emitServerMessage = (event, payload) => {
|
|
75
|
+
const [, options] = useAssistantSocket.mock.calls.at(-1);
|
|
76
|
+
act(() => options.onServerMessage(event, payload));
|
|
35
77
|
};
|
|
36
78
|
|
|
37
79
|
describe("<SuggestionsWidget />", () => {
|
|
38
|
-
|
|
80
|
+
const mockFetchSuggestions = jest.fn();
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
jest.clearAllMocks();
|
|
84
|
+
useAgentLayerRun.mockReturnValue({
|
|
85
|
+
trigger: mockFetchSuggestions,
|
|
86
|
+
data: null,
|
|
87
|
+
});
|
|
88
|
+
mockCheckAvailability.mockResolvedValue({ available: true });
|
|
89
|
+
useAvailabilityCheck.mockReturnValue({ trigger: mockCheckAvailability });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders nothing when suggestions are not available", async () => {
|
|
93
|
+
mockCheckAvailability.mockResolvedValue({ available: false });
|
|
39
94
|
const props = {
|
|
40
|
-
template
|
|
41
|
-
|
|
95
|
+
template,
|
|
96
|
+
suggestionParams: { template_id: 1, resource_type: "data_structure" },
|
|
42
97
|
};
|
|
43
|
-
const {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
98
|
+
const { queryByText } = render(<SuggestionsWidget {...props} />, {
|
|
99
|
+
messages,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await waitFor(() =>
|
|
103
|
+
expect(mockCheckAvailability).toHaveBeenCalledWith({
|
|
104
|
+
template_id: 1,
|
|
105
|
+
domain_ids: [],
|
|
106
|
+
resource_type: "data_structure",
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
expect(queryByText(/actions.ai_suggestion/i)).not.toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("ignores a stale availability response when the params change", async () => {
|
|
113
|
+
const notAllowed = Promise.resolve({ available: false });
|
|
114
|
+
// template 1 is allowed but settles after template 2's "not allowed"
|
|
115
|
+
mockCheckAvailability
|
|
116
|
+
.mockReturnValueOnce(notAllowed.then(() => ({ available: true })))
|
|
117
|
+
.mockReturnValueOnce(notAllowed);
|
|
118
|
+
|
|
119
|
+
const { rerender, queryByText } = render(
|
|
120
|
+
<SuggestionsWidget
|
|
121
|
+
template={template}
|
|
122
|
+
suggestionParams={{ template_id: 1, resource_type: "data_structure" }}
|
|
123
|
+
/>,
|
|
47
124
|
{ messages },
|
|
48
125
|
);
|
|
49
|
-
|
|
126
|
+
|
|
127
|
+
rerender(
|
|
128
|
+
<SuggestionsWidget
|
|
129
|
+
template={template}
|
|
130
|
+
suggestionParams={{ template_id: 2, resource_type: "data_structure" }}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Flush both availability responses (incl. the late, stale "allowed" one).
|
|
135
|
+
await act(async () => {
|
|
136
|
+
await Promise.resolve();
|
|
137
|
+
await Promise.resolve();
|
|
138
|
+
await Promise.resolve();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(queryByText(/actions.ai_suggestion/i)).not.toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("requests suggestions with suggestionParams and the entered prompt", async () => {
|
|
145
|
+
const props = { template, suggestionParams: { template_id: 7 } };
|
|
146
|
+
const { findByText } = render(<SuggestionsWidget {...props} />, {
|
|
147
|
+
messages,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await requestSuggestions(findByText);
|
|
151
|
+
|
|
152
|
+
await waitFor(() =>
|
|
153
|
+
expect(mockFetchSuggestions).toHaveBeenCalledWith({
|
|
154
|
+
workflow: "suggest_content",
|
|
155
|
+
params: { template_id: 7, prompt: "" },
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("disables the request control when enableRequestButton is false", async () => {
|
|
161
|
+
const props = {
|
|
162
|
+
template,
|
|
163
|
+
suggestionParams: { template_id: 1 },
|
|
164
|
+
enableRequestButton: false,
|
|
165
|
+
disabledButtonMessage: "name required",
|
|
166
|
+
};
|
|
167
|
+
const { findByText } = render(<SuggestionsWidget {...props} />, {
|
|
168
|
+
messages,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const button = await findByText(/actions.ai_suggestion/i);
|
|
172
|
+
expect(button.closest("button")).toBeDisabled();
|
|
50
173
|
});
|
|
51
174
|
|
|
52
|
-
it("
|
|
175
|
+
it("shows a spinner and disables the request control once requested", async () => {
|
|
176
|
+
const props = { template, suggestionParams: { template_id: 1 } };
|
|
177
|
+
const { findByText, container } = render(<SuggestionsWidget {...props} />, {
|
|
178
|
+
messages,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await requestSuggestions(findByText);
|
|
182
|
+
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
const button = container.querySelector("button");
|
|
185
|
+
expect(button).toBeDisabled();
|
|
186
|
+
expect(container.querySelector(".segment.loading")).toBeInTheDocument();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("connects the socket using the agent_state_id returned by the run", async () => {
|
|
191
|
+
useAgentLayerRun.mockReturnValue({
|
|
192
|
+
trigger: mockFetchSuggestions,
|
|
193
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const props = { template, suggestionParams: { template_id: 1 } };
|
|
197
|
+
render(<SuggestionsWidget {...props} />, { messages });
|
|
198
|
+
|
|
199
|
+
expect(useAssistantSocket).toHaveBeenCalledWith(
|
|
200
|
+
false,
|
|
201
|
+
expect.objectContaining({ conversationId: "state-1" }),
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("renders selectable suggestions once a priority-0 stream event arrives", async () => {
|
|
206
|
+
useAgentLayerRun.mockReturnValue({
|
|
207
|
+
trigger: mockFetchSuggestions,
|
|
208
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
209
|
+
});
|
|
210
|
+
|
|
53
211
|
const applySuggestions = jest.fn();
|
|
54
212
|
const props = {
|
|
55
|
-
|
|
213
|
+
template,
|
|
214
|
+
suggestionParams: { template_id: 1 },
|
|
56
215
|
applySuggestions,
|
|
57
|
-
template: {
|
|
58
|
-
content: [
|
|
59
|
-
{
|
|
60
|
-
fields: [
|
|
61
|
-
{
|
|
62
|
-
name: "editable_field",
|
|
63
|
-
label: "EditableField",
|
|
64
|
-
editable: true,
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: "Group2",
|
|
70
|
-
fields: [
|
|
71
|
-
{
|
|
72
|
-
name: "non_editable_field",
|
|
73
|
-
label: "NonEditableField",
|
|
74
|
-
editable: false,
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: "unselect_field",
|
|
78
|
-
label: "UnselectedField",
|
|
79
|
-
editable: true,
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
216
|
isModification: true,
|
|
86
217
|
};
|
|
87
|
-
const {
|
|
88
|
-
<
|
|
89
|
-
<SuggestionsWidget {...props} />
|
|
90
|
-
</Suspense>,
|
|
218
|
+
const { findByText, queryByText } = render(
|
|
219
|
+
<SuggestionsWidget {...props} />,
|
|
91
220
|
{ messages },
|
|
92
221
|
);
|
|
93
222
|
|
|
94
|
-
|
|
223
|
+
emitServerMessage("stream", {
|
|
224
|
+
priority: 0,
|
|
225
|
+
content: JSON.stringify({ result: mockSuggestions }),
|
|
226
|
+
});
|
|
227
|
+
|
|
95
228
|
await waitFor(() =>
|
|
96
|
-
|
|
229
|
+
// exact text: a regex match here would also match "NonEditableField"
|
|
230
|
+
expect(queryByText("EditableField")).toBeInTheDocument(),
|
|
97
231
|
);
|
|
98
232
|
|
|
99
|
-
|
|
233
|
+
await user.click(await findByText("UnselectedField"));
|
|
234
|
+
await user.click(await findByText("UnselectedField"));
|
|
235
|
+
await user.click(await findByText("UnselectedField"));
|
|
100
236
|
|
|
101
|
-
|
|
102
|
-
userEvent.click(await findByText(/UnselectedField/i));
|
|
103
|
-
userEvent.click(await findByText(/UnselectedField/i));
|
|
237
|
+
await user.click(await findByText(/actions.apply_ai_suggestion/i));
|
|
104
238
|
|
|
105
|
-
userEvent.click(await findByText(/actions.apply_ai_suggestion/i));
|
|
106
|
-
await waitFor(() =>
|
|
107
|
-
expect(
|
|
108
|
-
queryByText(/actions.apply_ai_suggestion/i),
|
|
109
|
-
).not.toBeInTheDocument(),
|
|
110
|
-
);
|
|
111
239
|
expect(applySuggestions).toHaveBeenCalledWith(
|
|
112
240
|
expect.objectContaining({
|
|
113
241
|
editable_field: { origin: "ai", value: "editable_field_value" },
|
|
114
242
|
}),
|
|
115
243
|
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("renders markdown suggestion values with the markdown reader", async () => {
|
|
247
|
+
useAgentLayerRun.mockReturnValue({
|
|
248
|
+
trigger: mockFetchSuggestions,
|
|
249
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
250
|
+
});
|
|
116
251
|
|
|
117
|
-
|
|
252
|
+
const markdownTemplate = {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
fields: [
|
|
256
|
+
{
|
|
257
|
+
name: "rich_field",
|
|
258
|
+
label: "RichField",
|
|
259
|
+
editable: true,
|
|
260
|
+
widget: "markdown",
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
const props = {
|
|
267
|
+
template: markdownTemplate,
|
|
268
|
+
suggestionParams: { template_id: 1 },
|
|
269
|
+
};
|
|
270
|
+
const { container, queryByText } = render(
|
|
271
|
+
<SuggestionsWidget {...props} />,
|
|
272
|
+
{
|
|
273
|
+
messages,
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
emitServerMessage("stream", {
|
|
278
|
+
priority: 0,
|
|
279
|
+
content: JSON.stringify({ result: { rich_field: "# Heading\n\nbody" } }),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await waitFor(() =>
|
|
283
|
+
expect(container.querySelector(".markdown-reader")).toBeInTheDocument(),
|
|
284
|
+
);
|
|
285
|
+
// rendered as HTML, so the raw markdown source is not present as text
|
|
286
|
+
expect(queryByText("# Heading")).not.toBeInTheDocument();
|
|
118
287
|
});
|
|
119
288
|
|
|
120
|
-
it("
|
|
289
|
+
it("returns to the request state after applying suggestions", async () => {
|
|
290
|
+
useAgentLayerRun.mockReturnValue({
|
|
291
|
+
trigger: mockFetchSuggestions,
|
|
292
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
293
|
+
});
|
|
294
|
+
|
|
121
295
|
const props = {
|
|
122
|
-
|
|
296
|
+
template,
|
|
297
|
+
suggestionParams: { template_id: 1 },
|
|
298
|
+
applySuggestions: jest.fn(),
|
|
123
299
|
};
|
|
124
|
-
const {
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
300
|
+
const { findByText, queryByText } = render(
|
|
301
|
+
<SuggestionsWidget {...props} />,
|
|
302
|
+
{ messages },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
emitServerMessage("stream", {
|
|
306
|
+
priority: 0,
|
|
307
|
+
content: JSON.stringify({ result: mockSuggestions }),
|
|
308
|
+
});
|
|
309
|
+
await waitFor(() =>
|
|
310
|
+
expect(queryByText("EditableField")).toBeInTheDocument(),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
await user.click(await findByText(/actions.apply_ai_suggestion/i));
|
|
314
|
+
|
|
315
|
+
await waitFor(() =>
|
|
316
|
+
expect(queryByText("EditableField")).not.toBeInTheDocument(),
|
|
317
|
+
);
|
|
318
|
+
const requestButton = (await findByText(/actions.ai_suggestion/i)).closest(
|
|
319
|
+
"button",
|
|
320
|
+
);
|
|
321
|
+
expect(requestButton).not.toBeDisabled();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("discards the suggestions and returns to the request state", async () => {
|
|
325
|
+
useAgentLayerRun.mockReturnValue({
|
|
326
|
+
trigger: mockFetchSuggestions,
|
|
327
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const props = { template, suggestionParams: { template_id: 1 } };
|
|
331
|
+
const { findByText, queryByText } = render(
|
|
332
|
+
<SuggestionsWidget {...props} />,
|
|
333
|
+
{ messages },
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
emitServerMessage("stream", {
|
|
337
|
+
priority: 0,
|
|
338
|
+
content: JSON.stringify({ result: mockSuggestions }),
|
|
339
|
+
});
|
|
340
|
+
await waitFor(() =>
|
|
341
|
+
expect(queryByText("EditableField")).toBeInTheDocument(),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
await user.click(await findByText(/actions.discard_ai_suggestion/i));
|
|
345
|
+
|
|
346
|
+
await waitFor(() =>
|
|
347
|
+
expect(queryByText("EditableField")).not.toBeInTheDocument(),
|
|
348
|
+
);
|
|
349
|
+
expect(queryByText(/actions.apply_ai_suggestion/i)).not.toBeInTheDocument();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("dismisses an error and returns to the request state", async () => {
|
|
353
|
+
useAgentLayerRun.mockReturnValue({
|
|
354
|
+
trigger: mockFetchSuggestions,
|
|
355
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const props = { template, suggestionParams: { template_id: 1 } };
|
|
359
|
+
const { findByText, queryByText } = render(
|
|
360
|
+
<SuggestionsWidget {...props} />,
|
|
361
|
+
{ messages },
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await requestSuggestions(findByText);
|
|
365
|
+
emitServerMessage("error", { priority: 0, error: "ERROR TEXT" });
|
|
366
|
+
await waitFor(() => expect(queryByText(/ERROR TEXT/i)).toBeInTheDocument());
|
|
367
|
+
|
|
368
|
+
await user.click(await findByText(/actions.discard_ai_suggestion/i));
|
|
369
|
+
|
|
370
|
+
await waitFor(() =>
|
|
371
|
+
expect(queryByText(/ERROR TEXT/i)).not.toBeInTheDocument(),
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("shows ThinkingOutLoud-driven log items while the agent is reasoning", async () => {
|
|
376
|
+
useAgentLayerRun.mockReturnValue({
|
|
377
|
+
trigger: mockFetchSuggestions,
|
|
378
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const props = { template, suggestionParams: { template_id: 1 } };
|
|
382
|
+
const { findByText, queryByText } = render(
|
|
383
|
+
<SuggestionsWidget {...props} />,
|
|
384
|
+
{ messages },
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
await requestSuggestions(findByText);
|
|
388
|
+
|
|
389
|
+
emitServerMessage("log", {
|
|
390
|
+
priority: 1,
|
|
391
|
+
source: "agent_x",
|
|
392
|
+
content: "Processing...",
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await waitFor(() =>
|
|
396
|
+
expect(queryByText(/Processing.../i)).toBeInTheDocument(),
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("renders error with message", async () => {
|
|
401
|
+
useAgentLayerRun.mockReturnValue({
|
|
402
|
+
trigger: mockFetchSuggestions,
|
|
403
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const props = { template, suggestionParams: { template_id: 1 } };
|
|
407
|
+
const { findByText, queryByText } = render(
|
|
408
|
+
<SuggestionsWidget {...props} />,
|
|
128
409
|
{ messages },
|
|
129
410
|
);
|
|
130
411
|
|
|
131
|
-
|
|
412
|
+
await requestSuggestions(findByText);
|
|
413
|
+
|
|
414
|
+
emitServerMessage("error", {
|
|
415
|
+
priority: 0,
|
|
416
|
+
error: { message: "ERROR MESSAGE" },
|
|
417
|
+
});
|
|
418
|
+
|
|
132
419
|
await waitFor(() =>
|
|
133
|
-
expect(queryByText(/
|
|
420
|
+
expect(queryByText(/ERROR MESSAGE/i)).toBeInTheDocument(),
|
|
134
421
|
);
|
|
135
|
-
expect(container).toMatchSnapshot();
|
|
136
422
|
});
|
|
137
423
|
|
|
138
424
|
it("renders error with text", async () => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
425
|
+
useAgentLayerRun.mockReturnValue({
|
|
426
|
+
trigger: mockFetchSuggestions,
|
|
427
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const props = { template, suggestionParams: { template_id: 1 } };
|
|
431
|
+
const { findByText, queryByText } = render(
|
|
432
|
+
<SuggestionsWidget {...props} />,
|
|
146
433
|
{ messages },
|
|
147
434
|
);
|
|
148
435
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
436
|
+
await requestSuggestions(findByText);
|
|
437
|
+
|
|
438
|
+
emitServerMessage("error", { priority: 0, error: "ERROR TEXT" });
|
|
439
|
+
|
|
440
|
+
await waitFor(() => expect(queryByText(/ERROR TEXT/i)).toBeInTheDocument());
|
|
152
441
|
});
|
|
153
442
|
});
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
1
2
|
import useSWRMutations from "swr/mutation";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { apiJsonOptions } from "@truedat/core/services/api";
|
|
4
|
+
import { API_AGENT_LAYER_RUN } from "../api";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// The suggestion availability gate is an OPTIONS request: the backend answers
|
|
7
|
+
// with an `Allow` header listing the methods enabled for the caller. POST being
|
|
8
|
+
// allowed means the "AI suggest content" button should be shown.
|
|
9
|
+
const isPostAllowed = ({ headers }) =>
|
|
10
|
+
_.includes("POST")(_.getOr("", "allow", headers));
|
|
11
|
+
|
|
12
|
+
export const useAvailabilityCheck = () =>
|
|
13
|
+
useSWRMutations(API_AGENT_LAYER_RUN, (url, { arg }) =>
|
|
14
|
+
apiJsonOptions(url, {
|
|
15
|
+
params: { workflow: "suggest_content", ...arg },
|
|
16
|
+
}).then((res) => ({ available: isPostAllowed(res) })),
|
|
8
17
|
);
|
|
9
|
-
};
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
-
|
|
3
|
-
exports[`<SuggestionsWidget /> component lifecycle 1`] = `
|
|
4
|
-
<div>
|
|
5
|
-
<div
|
|
6
|
-
class="ui segment"
|
|
7
|
-
>
|
|
8
|
-
<div
|
|
9
|
-
class="ui container"
|
|
10
|
-
style="display: flex; justify-content: space-between;"
|
|
11
|
-
>
|
|
12
|
-
<h4>
|
|
13
|
-
<i
|
|
14
|
-
aria-hidden="true"
|
|
15
|
-
class="lightbulb outline icon"
|
|
16
|
-
/>
|
|
17
|
-
header
|
|
18
|
-
</h4>
|
|
19
|
-
<div
|
|
20
|
-
class="ui hidden divider"
|
|
21
|
-
/>
|
|
22
|
-
</div>
|
|
23
|
-
<div
|
|
24
|
-
class="ui container"
|
|
25
|
-
>
|
|
26
|
-
<h5 />
|
|
27
|
-
<div
|
|
28
|
-
class="ui list"
|
|
29
|
-
role="list"
|
|
30
|
-
>
|
|
31
|
-
<div
|
|
32
|
-
class="item"
|
|
33
|
-
role="listitem"
|
|
34
|
-
style="display: flex; align-items: center; gap: 12px;"
|
|
35
|
-
>
|
|
36
|
-
<div
|
|
37
|
-
class="ui checked fitted checkbox"
|
|
38
|
-
>
|
|
39
|
-
<input
|
|
40
|
-
checked=""
|
|
41
|
-
class="hidden"
|
|
42
|
-
id="editable_field"
|
|
43
|
-
readonly=""
|
|
44
|
-
tabindex="0"
|
|
45
|
-
type="checkbox"
|
|
46
|
-
/>
|
|
47
|
-
<label
|
|
48
|
-
for="editable_field"
|
|
49
|
-
/>
|
|
50
|
-
</div>
|
|
51
|
-
<label
|
|
52
|
-
disabled=""
|
|
53
|
-
for="editable_field"
|
|
54
|
-
style="cursor: pointer;"
|
|
55
|
-
>
|
|
56
|
-
<div
|
|
57
|
-
class="header"
|
|
58
|
-
>
|
|
59
|
-
EditableField
|
|
60
|
-
</div>
|
|
61
|
-
<div
|
|
62
|
-
class="description"
|
|
63
|
-
>
|
|
64
|
-
editable_field_value
|
|
65
|
-
</div>
|
|
66
|
-
</label>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
<div
|
|
70
|
-
class="ui hidden divider"
|
|
71
|
-
/>
|
|
72
|
-
</div>
|
|
73
|
-
<div
|
|
74
|
-
class="ui container"
|
|
75
|
-
>
|
|
76
|
-
<h5>
|
|
77
|
-
Group2
|
|
78
|
-
</h5>
|
|
79
|
-
<div
|
|
80
|
-
class="ui list"
|
|
81
|
-
role="list"
|
|
82
|
-
>
|
|
83
|
-
<div
|
|
84
|
-
class="item"
|
|
85
|
-
role="listitem"
|
|
86
|
-
style="display: flex; align-items: center; gap: 12px;"
|
|
87
|
-
>
|
|
88
|
-
<div
|
|
89
|
-
class="ui disabled fitted checkbox"
|
|
90
|
-
>
|
|
91
|
-
<input
|
|
92
|
-
class="hidden"
|
|
93
|
-
disabled=""
|
|
94
|
-
id="non_editable_field"
|
|
95
|
-
readonly=""
|
|
96
|
-
tabindex="-1"
|
|
97
|
-
type="checkbox"
|
|
98
|
-
/>
|
|
99
|
-
<label
|
|
100
|
-
for="non_editable_field"
|
|
101
|
-
/>
|
|
102
|
-
</div>
|
|
103
|
-
<label
|
|
104
|
-
disabled=""
|
|
105
|
-
for="non_editable_field"
|
|
106
|
-
style="cursor: pointer;"
|
|
107
|
-
>
|
|
108
|
-
<div
|
|
109
|
-
class="header"
|
|
110
|
-
>
|
|
111
|
-
NonEditableField
|
|
112
|
-
</div>
|
|
113
|
-
<div
|
|
114
|
-
class="description"
|
|
115
|
-
>
|
|
116
|
-
non_editable_field_value
|
|
117
|
-
</div>
|
|
118
|
-
</label>
|
|
119
|
-
</div>
|
|
120
|
-
<div
|
|
121
|
-
class="item"
|
|
122
|
-
role="listitem"
|
|
123
|
-
style="display: flex; align-items: center; gap: 12px;"
|
|
124
|
-
>
|
|
125
|
-
<div
|
|
126
|
-
class="ui checked fitted checkbox"
|
|
127
|
-
>
|
|
128
|
-
<input
|
|
129
|
-
checked=""
|
|
130
|
-
class="hidden"
|
|
131
|
-
id="unselect_field"
|
|
132
|
-
readonly=""
|
|
133
|
-
tabindex="0"
|
|
134
|
-
type="checkbox"
|
|
135
|
-
/>
|
|
136
|
-
<label
|
|
137
|
-
for="unselect_field"
|
|
138
|
-
/>
|
|
139
|
-
</div>
|
|
140
|
-
<label
|
|
141
|
-
disabled=""
|
|
142
|
-
for="unselect_field"
|
|
143
|
-
style="cursor: pointer;"
|
|
144
|
-
>
|
|
145
|
-
<div
|
|
146
|
-
class="header"
|
|
147
|
-
>
|
|
148
|
-
UnselectedField
|
|
149
|
-
</div>
|
|
150
|
-
<div
|
|
151
|
-
class="description"
|
|
152
|
-
>
|
|
153
|
-
unselect_field_value
|
|
154
|
-
</div>
|
|
155
|
-
</label>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
<div
|
|
159
|
-
class="ui hidden divider"
|
|
160
|
-
/>
|
|
161
|
-
</div>
|
|
162
|
-
<div
|
|
163
|
-
class="ui right aligned container"
|
|
164
|
-
>
|
|
165
|
-
<button
|
|
166
|
-
class="ui primary button"
|
|
167
|
-
>
|
|
168
|
-
actions.apply_ai_suggestion
|
|
169
|
-
</button>
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
`;
|
|
174
|
-
|
|
175
|
-
exports[`<SuggestionsWidget /> component lifecycle 2`] = `
|
|
176
|
-
<div>
|
|
177
|
-
<div
|
|
178
|
-
class="ui segment"
|
|
179
|
-
>
|
|
180
|
-
<div
|
|
181
|
-
class="ui container"
|
|
182
|
-
style="display: flex; justify-content: space-between;"
|
|
183
|
-
>
|
|
184
|
-
<h4>
|
|
185
|
-
<i
|
|
186
|
-
aria-hidden="true"
|
|
187
|
-
class="lightbulb icon"
|
|
188
|
-
/>
|
|
189
|
-
header
|
|
190
|
-
</h4>
|
|
191
|
-
<div
|
|
192
|
-
class="ui hidden divider"
|
|
193
|
-
/>
|
|
194
|
-
<button
|
|
195
|
-
class="ui button"
|
|
196
|
-
>
|
|
197
|
-
actions.ai_suggestion
|
|
198
|
-
</button>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
`;
|
|
203
|
-
|
|
204
|
-
exports[`<SuggestionsWidget /> matches the latest snapshot 1`] = `
|
|
205
|
-
<div>
|
|
206
|
-
<div
|
|
207
|
-
class="ui segment"
|
|
208
|
-
>
|
|
209
|
-
<div
|
|
210
|
-
class="ui container"
|
|
211
|
-
style="display: flex; justify-content: space-between;"
|
|
212
|
-
>
|
|
213
|
-
<h4>
|
|
214
|
-
<i
|
|
215
|
-
aria-hidden="true"
|
|
216
|
-
class="lightbulb icon"
|
|
217
|
-
/>
|
|
218
|
-
header
|
|
219
|
-
</h4>
|
|
220
|
-
<div
|
|
221
|
-
class="ui hidden divider"
|
|
222
|
-
/>
|
|
223
|
-
<button
|
|
224
|
-
class="ui button"
|
|
225
|
-
>
|
|
226
|
-
actions.ai_suggestion
|
|
227
|
-
</button>
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
`;
|
|
232
|
-
|
|
233
|
-
exports[`<SuggestionsWidget /> renders error with message 1`] = `
|
|
234
|
-
<div>
|
|
235
|
-
<div
|
|
236
|
-
class="ui segment"
|
|
237
|
-
>
|
|
238
|
-
<div
|
|
239
|
-
class="ui container"
|
|
240
|
-
style="display: flex; justify-content: space-between;"
|
|
241
|
-
>
|
|
242
|
-
<h4>
|
|
243
|
-
<i
|
|
244
|
-
aria-hidden="true"
|
|
245
|
-
class="lightbulb icon"
|
|
246
|
-
/>
|
|
247
|
-
header
|
|
248
|
-
</h4>
|
|
249
|
-
<div
|
|
250
|
-
class="ui hidden divider"
|
|
251
|
-
/>
|
|
252
|
-
<button
|
|
253
|
-
class="ui button"
|
|
254
|
-
>
|
|
255
|
-
actions.ai_suggestion
|
|
256
|
-
</button>
|
|
257
|
-
</div>
|
|
258
|
-
<div
|
|
259
|
-
class="ui negative message"
|
|
260
|
-
>
|
|
261
|
-
<div
|
|
262
|
-
class="header"
|
|
263
|
-
>
|
|
264
|
-
error
|
|
265
|
-
</div>
|
|
266
|
-
<p>
|
|
267
|
-
ERROR MESSAGE
|
|
268
|
-
</p>
|
|
269
|
-
</div>
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
`;
|
|
273
|
-
|
|
274
|
-
exports[`<SuggestionsWidget /> renders error with text 1`] = `
|
|
275
|
-
<div>
|
|
276
|
-
<div
|
|
277
|
-
class="ui segment"
|
|
278
|
-
>
|
|
279
|
-
<div
|
|
280
|
-
class="ui container"
|
|
281
|
-
style="display: flex; justify-content: space-between;"
|
|
282
|
-
>
|
|
283
|
-
<h4>
|
|
284
|
-
<i
|
|
285
|
-
aria-hidden="true"
|
|
286
|
-
class="lightbulb icon"
|
|
287
|
-
/>
|
|
288
|
-
header
|
|
289
|
-
</h4>
|
|
290
|
-
<div
|
|
291
|
-
class="ui hidden divider"
|
|
292
|
-
/>
|
|
293
|
-
<button
|
|
294
|
-
class="ui button"
|
|
295
|
-
>
|
|
296
|
-
actions.ai_suggestion
|
|
297
|
-
</button>
|
|
298
|
-
</div>
|
|
299
|
-
<div
|
|
300
|
-
class="ui negative message"
|
|
301
|
-
>
|
|
302
|
-
<div
|
|
303
|
-
class="header"
|
|
304
|
-
>
|
|
305
|
-
error
|
|
306
|
-
</div>
|
|
307
|
-
<p>
|
|
308
|
-
ERROR TEXT
|
|
309
|
-
</p>
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
</div>
|
|
313
|
-
`;
|