@truedat/ai 8.5.4 → 8.5.7
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/AiRoutes.js +3 -0
- package/src/components/ConceptSuggestions.js +128 -0
- package/src/components/SuggestLinkButton.js +60 -0
- package/src/components/__tests__/ConceptSuggestions.spec.js +268 -0
- package/src/components/__tests__/SuggestLinkButton.spec.js +40 -0
- package/src/components/assistant/Assistant.js +186 -78
- package/src/components/assistant/AssistantChat.js +91 -30
- package/src/components/assistant/AssistantConversations.js +78 -0
- package/src/components/assistant/AssistantPage.js +139 -0
- package/src/components/assistant/AssistantPageChat.js +447 -0
- package/src/components/assistant/__tests__/Assistant.spec.js +15 -0
- package/src/components/assistant/__tests__/AssistantChat.spec.js +18 -0
- package/src/components/assistant/__tests__/AssistantConversations.spec.js +111 -0
- package/src/components/assistant/__tests__/__snapshots__/Assistant.spec.js.snap +5 -5
- package/src/components/assistant/__tests__/__snapshots__/AssistantChat.spec.js.snap +18 -8
- package/src/components/assistant/__tests__/__snapshots__/AssistantConversations.spec.js.snap +73 -0
- package/src/components/assistant/hooks/useAgentConversation.js +9 -3
- package/src/components/assistant/hooks/useAssistantSocket.js +36 -3
- package/src/components/index.js +2 -1
- package/src/hooks/__tests__/useAgentConversations.spec.js +83 -0
- package/src/hooks/useAgentConversations.js +15 -0
- package/src/styles/assistant.less +281 -121
- package/src/components/assistant/AssistantBubble.js +0 -56
- package/src/components/assistant/__tests__/AssistantBubble.spec.js +0 -14
- package/src/components/assistant/__tests__/__snapshots__/AssistantBubble.spec.js.snap +0 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/ai",
|
|
3
|
-
"version": "8.5.
|
|
3
|
+
"version": "8.5.7",
|
|
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.5.
|
|
53
|
+
"@truedat/test": "8.5.7",
|
|
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": "871357888d97c203cf009a04878e0277cff6af9b"
|
|
84
84
|
}
|
package/src/api.js
CHANGED
|
@@ -16,12 +16,14 @@ const API_SUGGESTIONS_REQUEST = "/api/suggestions/request";
|
|
|
16
16
|
const API_TRANSLATIONS_AVAILABILITY_CHECK =
|
|
17
17
|
"/api/translations/availability_check";
|
|
18
18
|
const API_TRANSLATIONS_REQUEST = "/api/translations/request";
|
|
19
|
+
const API_AGENT_LAYER_CONVERSATIONS = "/api/agent_layer/conversations";
|
|
19
20
|
const API_AGENT_LAYER_CONVERSATION = "/api/agent_layer/conversation";
|
|
20
21
|
const API_AGENT_LAYER_RUN = "/api/agent_layer/run";
|
|
21
22
|
const API_SOCKET_ENDPOINT = "/api/socket";
|
|
22
23
|
|
|
23
24
|
export {
|
|
24
25
|
API_ACTION,
|
|
26
|
+
API_AGENT_LAYER_CONVERSATIONS,
|
|
25
27
|
API_ACTIONS,
|
|
26
28
|
API_ACTIONS_SEARCH,
|
|
27
29
|
API_ACTION_SET_ACTIVE,
|
|
@@ -10,6 +10,7 @@ import Actions from "./actions/Actions";
|
|
|
10
10
|
import Action from "./actions/Action";
|
|
11
11
|
import ActionEdit from "./actions/ActionEdit";
|
|
12
12
|
import ActionNew from "./actions/ActionNew";
|
|
13
|
+
import AssistantPage from "./assistant/AssistantPage";
|
|
13
14
|
|
|
14
15
|
export default function AiRoutes() {
|
|
15
16
|
return (
|
|
@@ -52,6 +53,8 @@ export default function AiRoutes() {
|
|
|
52
53
|
<Route path={":id"} element={<Action />} />
|
|
53
54
|
</Route>
|
|
54
55
|
|
|
56
|
+
<Route path={"ai-assistant"} element={<AssistantPage />} />
|
|
57
|
+
|
|
55
58
|
<Route path="*" element={null} />
|
|
56
59
|
</Routes>
|
|
57
60
|
);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { useParams, useLocation } from "react-router";
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
import { FormattedMessage } from "react-intl";
|
|
5
|
+
import { Icon, Loader, Message } from "semantic-ui-react";
|
|
6
|
+
import { ConceptSelectorTable } from "@truedat/bg/concepts/relations/components/ConceptSelector";
|
|
7
|
+
import useAssistantSocket from "./assistant/hooks/useAssistantSocket";
|
|
8
|
+
import ThinkingOutLoud from "./ThinkingOutLoud";
|
|
9
|
+
import { useAgentLayerRun } from "../hooks/useAgentLayerRun";
|
|
10
|
+
import {
|
|
11
|
+
reasonColumnDefinition,
|
|
12
|
+
similarityColumnDefinition,
|
|
13
|
+
} from "./structureSuggestionColumns";
|
|
14
|
+
|
|
15
|
+
const extraColumns = [similarityColumnDefinition, reasonColumnDefinition];
|
|
16
|
+
|
|
17
|
+
export const ConceptSuggestions = ({ selectedConcept, handleConceptSelected }) => {
|
|
18
|
+
const { id } = useParams();
|
|
19
|
+
const { state } = useLocation();
|
|
20
|
+
const prompt = state?.prompt;
|
|
21
|
+
|
|
22
|
+
const { trigger: fetchSuggestions, data: runData } = useAgentLayerRun();
|
|
23
|
+
|
|
24
|
+
const [logItems, setLogItems] = useState([]);
|
|
25
|
+
const [concepts, setConcepts] = useState(null);
|
|
26
|
+
const [hasError, setHasError] = useState(false);
|
|
27
|
+
|
|
28
|
+
const agentStateId = runData?.data?.data?.agent_state_id
|
|
29
|
+
? String(runData.data.data.agent_state_id)
|
|
30
|
+
: null;
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (id) {
|
|
34
|
+
fetchSuggestions({
|
|
35
|
+
workflow: "suggest_structure_concept_links",
|
|
36
|
+
params: { structure_id: id, prompt },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}, [id, prompt]);
|
|
40
|
+
|
|
41
|
+
const handleServerMessage = useCallback((event, payload) => {
|
|
42
|
+
const priority = payload?.priority ?? null;
|
|
43
|
+
const source = payload?.source ?? null;
|
|
44
|
+
|
|
45
|
+
if (event === "stream" && Number(priority) === 0) {
|
|
46
|
+
const content = payload?.content;
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
const raw = typeof content === "string" ? JSON.parse(content) : content;
|
|
50
|
+
parsed = Array.isArray(raw) ? raw : (raw?.result ?? []);
|
|
51
|
+
} catch {
|
|
52
|
+
parsed = [];
|
|
53
|
+
}
|
|
54
|
+
setConcepts(parsed);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (event === "error" && Number(priority) !== 2) {
|
|
59
|
+
setHasError(true);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (event === "log" || (event === "error" && Number(priority) === 2)) {
|
|
64
|
+
const content =
|
|
65
|
+
event === "log"
|
|
66
|
+
? (() => {
|
|
67
|
+
const log = payload?.content;
|
|
68
|
+
return log && typeof log === "object"
|
|
69
|
+
? log.message ?? log.level ?? JSON.stringify(log)
|
|
70
|
+
: String(log ?? "");
|
|
71
|
+
})()
|
|
72
|
+
: (() => {
|
|
73
|
+
const err = payload?.error;
|
|
74
|
+
return err && typeof err === "object"
|
|
75
|
+
? err.message ?? err.reason ?? JSON.stringify(err)
|
|
76
|
+
: String(err ?? "");
|
|
77
|
+
})();
|
|
78
|
+
|
|
79
|
+
setLogItems((prev) => [
|
|
80
|
+
...prev,
|
|
81
|
+
{ eventType: event, content: content || " ", priority, source },
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
useAssistantSocket(!agentStateId, {
|
|
87
|
+
conversationId: agentStateId,
|
|
88
|
+
onServerMessage: handleServerMessage,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (hasError) {
|
|
92
|
+
return (
|
|
93
|
+
<Message icon negative>
|
|
94
|
+
<Icon name="warning circle" />
|
|
95
|
+
<Message.Content>
|
|
96
|
+
<Message.Header>
|
|
97
|
+
<FormattedMessage id="concepts.suggestions.error.header" />
|
|
98
|
+
</Message.Header>
|
|
99
|
+
<FormattedMessage id="concepts.suggestions.error.body" />
|
|
100
|
+
</Message.Content>
|
|
101
|
+
</Message>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (concepts !== null) {
|
|
106
|
+
return (
|
|
107
|
+
<ConceptSelectorTable
|
|
108
|
+
concepts={concepts}
|
|
109
|
+
selectedConcept={selectedConcept}
|
|
110
|
+
handleConceptSelected={handleConceptSelected}
|
|
111
|
+
extraColumns={extraColumns}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (logItems.length === 0) {
|
|
117
|
+
return <Loader active inline="centered" />;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return <ThinkingOutLoud items={logItems} active={true} />;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
ConceptSuggestions.propTypes = {
|
|
124
|
+
selectedConcept: PropTypes.object,
|
|
125
|
+
handleConceptSelected: PropTypes.func,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default ConceptSuggestions;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { Button, Form, Popup } from "semantic-ui-react";
|
|
4
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
5
|
+
|
|
6
|
+
export const SuggestLinkButton = ({ onSubmit, floated }) => {
|
|
7
|
+
const { formatMessage } = useIntl();
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const [prompt, setPrompt] = useState("");
|
|
10
|
+
|
|
11
|
+
const handleSubmit = () => {
|
|
12
|
+
setOpen(false);
|
|
13
|
+
onSubmit(prompt);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Popup
|
|
18
|
+
on="click"
|
|
19
|
+
open={open}
|
|
20
|
+
onOpen={() => setOpen(true)}
|
|
21
|
+
onClose={() => setOpen(false)}
|
|
22
|
+
content={
|
|
23
|
+
<Form style={{ minWidth: "300px" }}>
|
|
24
|
+
<Form.Field>
|
|
25
|
+
<label>{formatMessage({ id: "links.suggest.prompt.label" })}</label>
|
|
26
|
+
<Form.Input
|
|
27
|
+
value={prompt}
|
|
28
|
+
onChange={(_, { value }) => setPrompt(value)}
|
|
29
|
+
placeholder={formatMessage({
|
|
30
|
+
id: "links.suggest.prompt.placeholder",
|
|
31
|
+
})}
|
|
32
|
+
/>
|
|
33
|
+
</Form.Field>
|
|
34
|
+
<Button
|
|
35
|
+
primary
|
|
36
|
+
content={formatMessage({ id: "links.suggest.submit" })}
|
|
37
|
+
onClick={handleSubmit}
|
|
38
|
+
/>
|
|
39
|
+
</Form>
|
|
40
|
+
}
|
|
41
|
+
position="bottom center"
|
|
42
|
+
trigger={
|
|
43
|
+
<Button
|
|
44
|
+
secondary
|
|
45
|
+
active={open}
|
|
46
|
+
floated={floated}
|
|
47
|
+
icon="lightbulb outline"
|
|
48
|
+
content={<FormattedMessage id="links.actions.suggest" />}
|
|
49
|
+
/>
|
|
50
|
+
}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
SuggestLinkButton.propTypes = {
|
|
56
|
+
onSubmit: PropTypes.func.isRequired,
|
|
57
|
+
floated: PropTypes.string,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default SuggestLinkButton;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { act, waitFor } from "@testing-library/react";
|
|
2
|
+
import { useParams, useLocation } from "react-router";
|
|
3
|
+
import { render } from "@truedat/test/render";
|
|
4
|
+
import { ConceptSelectorTable } from "@truedat/bg/concepts/relations/components/ConceptSelector";
|
|
5
|
+
import useAssistantSocket from "../assistant/hooks/useAssistantSocket";
|
|
6
|
+
import ThinkingOutLoud from "../ThinkingOutLoud";
|
|
7
|
+
import { useAgentLayerRun } from "../../hooks/useAgentLayerRun";
|
|
8
|
+
import ConceptSuggestions from "../ConceptSuggestions";
|
|
9
|
+
|
|
10
|
+
jest.mock("react-router", () => ({
|
|
11
|
+
...jest.requireActual("react-router"),
|
|
12
|
+
useParams: jest.fn(),
|
|
13
|
+
useLocation: jest.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock("@truedat/bg/concepts/relations/components/ConceptSelector", () => ({
|
|
17
|
+
ConceptSelectorTable: jest.fn(() => <div>MockConceptSelectorTable</div>),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock("../ThinkingOutLoud", () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
default: jest.fn(() => <div>MockThinkingOutLoud</div>),
|
|
23
|
+
getSourceLabel: jest.fn((s) => s),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock("../assistant/hooks/useAssistantSocket", () => jest.fn());
|
|
27
|
+
|
|
28
|
+
jest.mock("../../hooks/useAgentLayerRun", () => ({
|
|
29
|
+
useAgentLayerRun: jest.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe("ConceptSuggestions", () => {
|
|
33
|
+
const mockFetchSuggestions = jest.fn();
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.clearAllMocks();
|
|
37
|
+
|
|
38
|
+
useParams.mockReturnValue({ id: "42" });
|
|
39
|
+
useLocation.mockReturnValue({ state: { prompt: "find concepts" } });
|
|
40
|
+
useAssistantSocket.mockImplementation(() => {});
|
|
41
|
+
|
|
42
|
+
useAgentLayerRun.mockReturnValue({
|
|
43
|
+
trigger: mockFetchSuggestions,
|
|
44
|
+
data: null,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("calls fetchSuggestions on mount with structure_id and prompt", async () => {
|
|
49
|
+
render(
|
|
50
|
+
<ConceptSuggestions
|
|
51
|
+
selectedConcept={null}
|
|
52
|
+
handleConceptSelected={jest.fn()}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await waitFor(() =>
|
|
57
|
+
expect(mockFetchSuggestions).toHaveBeenCalledWith({
|
|
58
|
+
workflow: "suggest_structure_concept_links",
|
|
59
|
+
params: { structure_id: "42", prompt: "find concepts" },
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("calls fetchSuggestions without prompt when prompt is missing", async () => {
|
|
65
|
+
useLocation.mockReturnValue({ state: null });
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<ConceptSuggestions
|
|
69
|
+
selectedConcept={null}
|
|
70
|
+
handleConceptSelected={jest.fn()}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await waitFor(() =>
|
|
75
|
+
expect(mockFetchSuggestions).toHaveBeenCalledWith({
|
|
76
|
+
workflow: "suggest_structure_concept_links",
|
|
77
|
+
params: { structure_id: "42", prompt: undefined },
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("shows spinner while waiting for first log item", () => {
|
|
83
|
+
useAgentLayerRun.mockReturnValue({
|
|
84
|
+
trigger: mockFetchSuggestions,
|
|
85
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const rendered = render(
|
|
89
|
+
<ConceptSuggestions
|
|
90
|
+
selectedConcept={null}
|
|
91
|
+
handleConceptSelected={jest.fn()}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(rendered.queryByText(/mockthinkingoutloud/i)).not.toBeInTheDocument();
|
|
96
|
+
expect(rendered.queryByText(/mockconceptselectortable/i)).not.toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("shows ThinkingOutLoud once log items arrive", async () => {
|
|
100
|
+
let capturedOnServerMessage;
|
|
101
|
+
|
|
102
|
+
useAssistantSocket.mockImplementation((_, { onServerMessage }) => {
|
|
103
|
+
capturedOnServerMessage = onServerMessage;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
useAgentLayerRun.mockReturnValue({
|
|
107
|
+
trigger: mockFetchSuggestions,
|
|
108
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const rendered = render(
|
|
112
|
+
<ConceptSuggestions
|
|
113
|
+
selectedConcept={null}
|
|
114
|
+
handleConceptSelected={jest.fn()}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
act(() => {
|
|
119
|
+
capturedOnServerMessage("log", {
|
|
120
|
+
priority: 1,
|
|
121
|
+
source: "agent_x",
|
|
122
|
+
content: "Processing...",
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await waitFor(() =>
|
|
127
|
+
expect(rendered.queryByText(/mockthinkingoutloud/i)).toBeInTheDocument()
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("disables socket when agent_state_id is not yet available", () => {
|
|
132
|
+
render(
|
|
133
|
+
<ConceptSuggestions
|
|
134
|
+
selectedConcept={null}
|
|
135
|
+
handleConceptSelected={jest.fn()}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(useAssistantSocket).toHaveBeenCalledWith(
|
|
140
|
+
true,
|
|
141
|
+
expect.objectContaining({ conversationId: null })
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("connects socket with agent_state_id when available", () => {
|
|
146
|
+
useAgentLayerRun.mockReturnValue({
|
|
147
|
+
trigger: mockFetchSuggestions,
|
|
148
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
render(
|
|
152
|
+
<ConceptSuggestions
|
|
153
|
+
selectedConcept={null}
|
|
154
|
+
handleConceptSelected={jest.fn()}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(useAssistantSocket).toHaveBeenCalledWith(
|
|
159
|
+
false,
|
|
160
|
+
expect.objectContaining({ conversationId: "state-1" })
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("shows ConceptSelectorTable when stream priority-0 arrives", async () => {
|
|
165
|
+
const concepts = [{ id: "c1" }, { id: "c2" }];
|
|
166
|
+
let capturedOnServerMessage;
|
|
167
|
+
|
|
168
|
+
useAssistantSocket.mockImplementation((_, { onServerMessage }) => {
|
|
169
|
+
capturedOnServerMessage = onServerMessage;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
useAgentLayerRun.mockReturnValue({
|
|
173
|
+
trigger: mockFetchSuggestions,
|
|
174
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const rendered = render(
|
|
178
|
+
<ConceptSuggestions
|
|
179
|
+
selectedConcept={null}
|
|
180
|
+
handleConceptSelected={jest.fn()}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
act(() => {
|
|
185
|
+
capturedOnServerMessage("stream", {
|
|
186
|
+
priority: 0,
|
|
187
|
+
content: JSON.stringify({ result: concepts }),
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await waitFor(() =>
|
|
192
|
+
expect(rendered.queryByText(/mockconceptselectortable/i)).toBeInTheDocument()
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(rendered.queryByText(/mockthinkingoutloud/i)).not.toBeInTheDocument();
|
|
196
|
+
|
|
197
|
+
const props = ConceptSelectorTable.mock.calls.at(-1)[0];
|
|
198
|
+
expect(props.concepts).toEqual(concepts);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("shows error message when error priority-0 arrives", async () => {
|
|
202
|
+
let capturedOnServerMessage;
|
|
203
|
+
|
|
204
|
+
useAssistantSocket.mockImplementation((_, { onServerMessage }) => {
|
|
205
|
+
capturedOnServerMessage = onServerMessage;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
useAgentLayerRun.mockReturnValue({
|
|
209
|
+
trigger: mockFetchSuggestions,
|
|
210
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const rendered = render(
|
|
214
|
+
<ConceptSuggestions
|
|
215
|
+
selectedConcept={null}
|
|
216
|
+
handleConceptSelected={jest.fn()}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
act(() => {
|
|
221
|
+
capturedOnServerMessage("error", {
|
|
222
|
+
priority: 0,
|
|
223
|
+
error: { message: "Something failed" },
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await waitFor(() =>
|
|
228
|
+
expect(rendered.queryByText(/mockthinkingoutloud/i)).not.toBeInTheDocument()
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(rendered.queryByText(/mockconceptselectortable/i)).not.toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("adds log events to ThinkingOutLoud items", async () => {
|
|
235
|
+
let capturedOnServerMessage;
|
|
236
|
+
|
|
237
|
+
useAssistantSocket.mockImplementation((_, { onServerMessage }) => {
|
|
238
|
+
capturedOnServerMessage = onServerMessage;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
useAgentLayerRun.mockReturnValue({
|
|
242
|
+
trigger: mockFetchSuggestions,
|
|
243
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
render(
|
|
247
|
+
<ConceptSuggestions
|
|
248
|
+
selectedConcept={null}
|
|
249
|
+
handleConceptSelected={jest.fn()}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
act(() => {
|
|
254
|
+
capturedOnServerMessage("log", {
|
|
255
|
+
priority: 1,
|
|
256
|
+
source: "agent_x",
|
|
257
|
+
content: "Processing...",
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
const lastCall = ThinkingOutLoud.mock.calls.at(-1)[0];
|
|
263
|
+
expect(lastCall.items).toHaveLength(1);
|
|
264
|
+
expect(lastCall.items[0].content).toBe("Processing...");
|
|
265
|
+
expect(lastCall.items[0].eventType).toBe("log");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import { SuggestLinkButton } from "../SuggestLinkButton";
|
|
4
|
+
|
|
5
|
+
jest.mock("react-intl", () => ({
|
|
6
|
+
...jest.requireActual("react-intl"),
|
|
7
|
+
useIntl: () => ({ formatMessage: ({ id }) => id }),
|
|
8
|
+
FormattedMessage: ({ id }) => id,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("SuggestLinkButton", () => {
|
|
12
|
+
it("renders the trigger button", () => {
|
|
13
|
+
render(<SuggestLinkButton onSubmit={jest.fn()} />);
|
|
14
|
+
expect(screen.getByText("links.actions.suggest")).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("calls onSubmit with the prompt value when submitted", async () => {
|
|
18
|
+
const onSubmit = jest.fn();
|
|
19
|
+
render(<SuggestLinkButton onSubmit={onSubmit} />);
|
|
20
|
+
|
|
21
|
+
fireEvent.click(screen.getByText("links.actions.suggest"));
|
|
22
|
+
|
|
23
|
+
const input = screen.getByPlaceholderText("links.suggest.prompt.placeholder");
|
|
24
|
+
fireEvent.change(input, { target: { value: "my prompt" } });
|
|
25
|
+
|
|
26
|
+
fireEvent.click(screen.getByText("links.suggest.submit"));
|
|
27
|
+
|
|
28
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith("my prompt"));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("calls onSubmit with empty string when no prompt entered", async () => {
|
|
32
|
+
const onSubmit = jest.fn();
|
|
33
|
+
render(<SuggestLinkButton onSubmit={onSubmit} />);
|
|
34
|
+
|
|
35
|
+
fireEvent.click(screen.getByText("links.actions.suggest"));
|
|
36
|
+
fireEvent.click(screen.getByText("links.suggest.submit"));
|
|
37
|
+
|
|
38
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(""));
|
|
39
|
+
});
|
|
40
|
+
});
|