@truedat/ai 8.4.8 → 8.5.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 +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/constants.js +5 -0
- package/src/components/index.js +2 -1
- package/src/components/providers/ProviderEditor.js +41 -1
- package/src/components/providers/Providers.js +15 -0
- package/src/components/providers/__tests__/__snapshots__/ProviderEditor.spec.js.snap +332 -0
- package/src/components/providers/providerProperties/Anthropic.js +113 -0
- package/src/components/providers/providerProperties/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/ai",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.5.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.4.
|
|
53
|
+
"@truedat/test": "8.4.9",
|
|
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": "c09665b24b178f5002c4f1d93ac4ce81e16bc62f"
|
|
84
84
|
}
|
package/src/api.js
CHANGED
|
@@ -17,6 +17,7 @@ const API_TRANSLATIONS_AVAILABILITY_CHECK =
|
|
|
17
17
|
"/api/translations/availability_check";
|
|
18
18
|
const API_TRANSLATIONS_REQUEST = "/api/translations/request";
|
|
19
19
|
const API_AGENT_LAYER_CONVERSATION = "/api/agent_layer/conversation";
|
|
20
|
+
const API_AGENT_LAYER_RUN = "/api/agent_layer/run";
|
|
20
21
|
const API_SOCKET_ENDPOINT = "/api/socket";
|
|
21
22
|
|
|
22
23
|
export {
|
|
@@ -37,5 +38,6 @@ export {
|
|
|
37
38
|
API_TRANSLATIONS_AVAILABILITY_CHECK,
|
|
38
39
|
API_TRANSLATIONS_REQUEST,
|
|
39
40
|
API_AGENT_LAYER_CONVERSATION,
|
|
41
|
+
API_AGENT_LAYER_RUN,
|
|
40
42
|
API_SOCKET_ENDPOINT,
|
|
41
43
|
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
2
|
+
import { useParams, useLocation } from "react-router";
|
|
3
|
+
import { useSelector } from "react-redux";
|
|
4
|
+
import PropTypes from "prop-types";
|
|
5
|
+
import { FormattedMessage } from "react-intl";
|
|
6
|
+
import { Icon, Loader, Message } from "semantic-ui-react";
|
|
7
|
+
import { defaultColumnsForStructureSelector } from "@truedat/dd/selectors";
|
|
8
|
+
import StructuresSearchResults from "@truedat/dd/components/StructuresSearchResults";
|
|
9
|
+
import useAssistantSocket from "./assistant/hooks/useAssistantSocket";
|
|
10
|
+
import ThinkingOutLoud from "./ThinkingOutLoud";
|
|
11
|
+
import { useAgentLayerRun } from "../hooks/useAgentLayerRun";
|
|
12
|
+
import {
|
|
13
|
+
reasonColumnDefinition,
|
|
14
|
+
similarityColumnDefinition,
|
|
15
|
+
} from "./structureSuggestionColumns";
|
|
16
|
+
|
|
17
|
+
export const StructureSuggestions = ({
|
|
18
|
+
selectedStructure,
|
|
19
|
+
handleSelectedStructure,
|
|
20
|
+
selectable,
|
|
21
|
+
}) => {
|
|
22
|
+
const { business_concept_id: id } = useParams();
|
|
23
|
+
const { state } = useLocation();
|
|
24
|
+
const prompt = state?.prompt;
|
|
25
|
+
|
|
26
|
+
const baseColumns = useSelector(defaultColumnsForStructureSelector);
|
|
27
|
+
const columns = useMemo(
|
|
28
|
+
() => [
|
|
29
|
+
...baseColumns,
|
|
30
|
+
similarityColumnDefinition,
|
|
31
|
+
reasonColumnDefinition,
|
|
32
|
+
],
|
|
33
|
+
[baseColumns]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const { trigger: fetchSuggestions, data: runData } = useAgentLayerRun();
|
|
37
|
+
|
|
38
|
+
const [logItems, setLogItems] = useState([]);
|
|
39
|
+
const [structures, setStructures] = useState(null);
|
|
40
|
+
const [hasError, setHasError] = useState(false);
|
|
41
|
+
|
|
42
|
+
const agentStateId = runData?.data?.data?.agent_state_id
|
|
43
|
+
? String(runData.data.data.agent_state_id)
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (id) {
|
|
48
|
+
fetchSuggestions({
|
|
49
|
+
workflow: "suggest_business_concept_structures_links",
|
|
50
|
+
params: { business_concept_id: id, prompt },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}, [id, prompt]);
|
|
54
|
+
|
|
55
|
+
const handleServerMessage = useCallback((event, payload) => {
|
|
56
|
+
const priority = payload?.priority ?? null;
|
|
57
|
+
const source = payload?.source ?? null;
|
|
58
|
+
|
|
59
|
+
if (event === "stream" && Number(priority) === 0) {
|
|
60
|
+
const content = payload?.content;
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
const raw = typeof content === "string" ? JSON.parse(content) : content;
|
|
64
|
+
parsed = Array.isArray(raw) ? raw : (raw?.result ?? []);
|
|
65
|
+
} catch {
|
|
66
|
+
parsed = [];
|
|
67
|
+
}
|
|
68
|
+
setStructures(parsed);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (event === "error" && Number(priority) !== 2) {
|
|
73
|
+
setHasError(true);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (event === "log" || (event === "error" && Number(priority) === 2)) {
|
|
78
|
+
const content =
|
|
79
|
+
event === "log"
|
|
80
|
+
? (() => {
|
|
81
|
+
const log = payload?.content;
|
|
82
|
+
return log && typeof log === "object"
|
|
83
|
+
? log.message ?? log.level ?? JSON.stringify(log)
|
|
84
|
+
: String(log ?? "");
|
|
85
|
+
})()
|
|
86
|
+
: (() => {
|
|
87
|
+
const err = payload?.error;
|
|
88
|
+
return err && typeof err === "object"
|
|
89
|
+
? err.message ?? err.reason ?? JSON.stringify(err)
|
|
90
|
+
: String(err ?? "");
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
setLogItems((prev) => [
|
|
94
|
+
...prev,
|
|
95
|
+
{ eventType: event, content: content || " ", priority, source },
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
useAssistantSocket(!agentStateId, {
|
|
101
|
+
conversationId: agentStateId,
|
|
102
|
+
onServerMessage: handleServerMessage,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (hasError) {
|
|
106
|
+
return (
|
|
107
|
+
<Message icon negative>
|
|
108
|
+
<Icon name="warning circle" />
|
|
109
|
+
<Message.Content>
|
|
110
|
+
<Message.Header>
|
|
111
|
+
<FormattedMessage id="structures.suggestions.error.header" />
|
|
112
|
+
</Message.Header>
|
|
113
|
+
<FormattedMessage id="structures.suggestions.error.body" />
|
|
114
|
+
</Message.Content>
|
|
115
|
+
</Message>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (structures !== null) {
|
|
120
|
+
return (
|
|
121
|
+
<StructuresSearchResults
|
|
122
|
+
columns={columns}
|
|
123
|
+
size="small"
|
|
124
|
+
structures={structures}
|
|
125
|
+
selectedStructure={selectedStructure}
|
|
126
|
+
onSelect={handleSelectedStructure}
|
|
127
|
+
labelResults={false}
|
|
128
|
+
pagination={false}
|
|
129
|
+
selectable={selectable}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (logItems.length === 0) {
|
|
135
|
+
return <Loader active inline="centered" />;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return <ThinkingOutLoud items={logItems} active={true} />;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
StructureSuggestions.propTypes = {
|
|
142
|
+
handleSelectedStructure: PropTypes.func,
|
|
143
|
+
selectedStructure: PropTypes.object,
|
|
144
|
+
selectable: PropTypes.bool,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default StructureSuggestions;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useState, useMemo, useRef, useEffect, useLayoutEffect } from "react";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { Icon } from "semantic-ui-react";
|
|
4
|
+
|
|
5
|
+
/** Converts e.g. "workflow_normalizer_ff83ae63.agent_normalize_10015c7d" → "Agent Normalize" */
|
|
6
|
+
export function getSourceLabel(source) {
|
|
7
|
+
if (!source) return "";
|
|
8
|
+
const segment = source.includes(".") ? source.split(".").pop() : source;
|
|
9
|
+
const withoutUuid = segment.replace(/_[0-9a-f]{8,}$/i, "");
|
|
10
|
+
return withoutUuid
|
|
11
|
+
.split("_")
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
14
|
+
.join(" ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Groups flat log items into sections by priority-1 source.
|
|
19
|
+
* priority-2 log items are nested under the last priority-1 section.
|
|
20
|
+
* `error` events with priority 2 are inline errors in the thinking block (styled in red).
|
|
21
|
+
*/
|
|
22
|
+
function buildSections(logItems) {
|
|
23
|
+
const sections = [];
|
|
24
|
+
let current = null;
|
|
25
|
+
logItems.forEach((item) => {
|
|
26
|
+
const p = item.priority ?? 1;
|
|
27
|
+
const isThinkingInlineError = item.eventType === "error" && Number(p) === 2;
|
|
28
|
+
if (isThinkingInlineError) {
|
|
29
|
+
if (!current) {
|
|
30
|
+
current = { source: null, label: "Processing", items: [] };
|
|
31
|
+
}
|
|
32
|
+
current.items.push({ ...item, level: 3 });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const level = p <= 1 ? 1 : 2;
|
|
36
|
+
if (level === 1) {
|
|
37
|
+
if (!current || current.source !== item.source) {
|
|
38
|
+
if (current) sections.push(current);
|
|
39
|
+
current = {
|
|
40
|
+
source: item.source,
|
|
41
|
+
label: getSourceLabel(item.source),
|
|
42
|
+
items: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
current.items.push({ ...item, level: 1 });
|
|
46
|
+
} else {
|
|
47
|
+
if (!current) {
|
|
48
|
+
current = { source: null, label: "Processing", items: [] };
|
|
49
|
+
}
|
|
50
|
+
current.items.push({ ...item, level: 2 });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (current) sections.push(current);
|
|
54
|
+
return sections;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ThinkingSection({ section, isLast, active, collapsed }) {
|
|
58
|
+
const [open, setOpen] = useState(!collapsed);
|
|
59
|
+
const bodyRef = useRef(null);
|
|
60
|
+
const isActive = active && isLast;
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (collapsed) setOpen(false);
|
|
64
|
+
}, [collapsed]);
|
|
65
|
+
|
|
66
|
+
useLayoutEffect(() => {
|
|
67
|
+
if (!open) return;
|
|
68
|
+
const el = bodyRef.current;
|
|
69
|
+
if (!el) return;
|
|
70
|
+
el.scrollTop = el.scrollHeight;
|
|
71
|
+
}, [open, section.items]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="thinking-section">
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
className="thinking-section__header"
|
|
78
|
+
onClick={() => setOpen((o) => !o)}
|
|
79
|
+
>
|
|
80
|
+
<Icon name={open ? "chevron down" : "chevron right"} />
|
|
81
|
+
<span className="thinking-section__label">{section.label}</span>
|
|
82
|
+
{isActive && (
|
|
83
|
+
<span className="thinking-section__spinner">
|
|
84
|
+
<span className="thinking-section__spinner-dot" />
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
</button>
|
|
88
|
+
{open && (
|
|
89
|
+
<div ref={bodyRef} className="thinking-section__body">
|
|
90
|
+
{section.items.map((item, i) => (
|
|
91
|
+
<div
|
|
92
|
+
key={i}
|
|
93
|
+
className={`thinking-section__item thinking-section__item--${item.level}`}
|
|
94
|
+
>
|
|
95
|
+
{item.level === 2 && (
|
|
96
|
+
<span className="thinking-section__item-source">
|
|
97
|
+
{getSourceLabel(item.source)}:{" "}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
{item.level === 3 && item.source && (
|
|
101
|
+
<span className="thinking-section__item-source">
|
|
102
|
+
{getSourceLabel(item.source)}:{" "}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
{item.content}
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ThinkingSection.propTypes = {
|
|
115
|
+
section: PropTypes.shape({
|
|
116
|
+
source: PropTypes.string,
|
|
117
|
+
label: PropTypes.string,
|
|
118
|
+
items: PropTypes.array,
|
|
119
|
+
}).isRequired,
|
|
120
|
+
isLast: PropTypes.bool,
|
|
121
|
+
active: PropTypes.bool,
|
|
122
|
+
collapsed: PropTypes.bool,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Renders streamed agent log lines (thinking out loud) grouped into collapsible sections.
|
|
127
|
+
* Expects items shaped like assistant log events: `eventType` "log" or inline "error" with priority 2.
|
|
128
|
+
*/
|
|
129
|
+
const ThinkingOutLoud = ({ items, active, collapsed }) => {
|
|
130
|
+
const sections = useMemo(() => buildSections(items), [items]);
|
|
131
|
+
return (
|
|
132
|
+
<div className="thinking-block">
|
|
133
|
+
{sections.map((section, i) => (
|
|
134
|
+
<ThinkingSection
|
|
135
|
+
key={`${section.source ?? "unknown"}-${i}`}
|
|
136
|
+
section={section}
|
|
137
|
+
isLast={i === sections.length - 1}
|
|
138
|
+
active={active}
|
|
139
|
+
collapsed={collapsed}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
ThinkingOutLoud.propTypes = {
|
|
147
|
+
items: PropTypes.array.isRequired,
|
|
148
|
+
active: PropTypes.bool,
|
|
149
|
+
collapsed: PropTypes.bool,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
ThinkingOutLoud.defaultProps = {
|
|
153
|
+
active: false,
|
|
154
|
+
collapsed: false,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export default ThinkingOutLoud;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { act, waitFor } from "@testing-library/react";
|
|
2
|
+
import { useParams, useLocation } from "react-router";
|
|
3
|
+
import { useSelector } from "react-redux";
|
|
4
|
+
import { render } from "@truedat/test/render";
|
|
5
|
+
import StructuresSearchResults from "@truedat/dd/components/StructuresSearchResults";
|
|
6
|
+
import useAssistantSocket from "../assistant/hooks/useAssistantSocket";
|
|
7
|
+
import ThinkingOutLoud from "../ThinkingOutLoud";
|
|
8
|
+
import { useAgentLayerRun } from "../../hooks/useAgentLayerRun";
|
|
9
|
+
import StructureSuggestions from "../StructureSuggestions";
|
|
10
|
+
|
|
11
|
+
jest.mock("react-router", () => ({
|
|
12
|
+
...jest.requireActual("react-router"),
|
|
13
|
+
useParams: jest.fn(),
|
|
14
|
+
useLocation: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.mock("react-redux", () => ({
|
|
18
|
+
...jest.requireActual("react-redux"),
|
|
19
|
+
useSelector: jest.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock("@truedat/dd/selectors", () => ({
|
|
23
|
+
defaultColumnsForStructureSelector: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock("@truedat/dd/components/StructuresSearchResults", () =>
|
|
27
|
+
jest.fn(() => <div>MockSearchResults</div>)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
jest.mock("../ThinkingOutLoud", () => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
default: jest.fn(() => <div>MockThinkingOutLoud</div>),
|
|
33
|
+
getSourceLabel: jest.fn((s) => s),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
jest.mock("../assistant/hooks/useAssistantSocket", () => jest.fn());
|
|
37
|
+
|
|
38
|
+
jest.mock("../../hooks/useAgentLayerRun", () => ({
|
|
39
|
+
useAgentLayerRun: jest.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe("StructureSuggestions", () => {
|
|
43
|
+
const mockFetchSuggestions = jest.fn();
|
|
44
|
+
const mockColumns = [{ name: "name", fieldSelector: (x) => x.name }];
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
jest.clearAllMocks();
|
|
48
|
+
|
|
49
|
+
useParams.mockReturnValue({ business_concept_id: "123" });
|
|
50
|
+
useLocation.mockReturnValue({ state: { prompt: "find structures" } });
|
|
51
|
+
useSelector.mockReturnValue(mockColumns);
|
|
52
|
+
useAssistantSocket.mockImplementation(() => {});
|
|
53
|
+
|
|
54
|
+
useAgentLayerRun.mockReturnValue({
|
|
55
|
+
trigger: mockFetchSuggestions,
|
|
56
|
+
data: null,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("calls fetchSuggestions on mount with business_concept_id and prompt", async () => {
|
|
61
|
+
render(
|
|
62
|
+
<StructureSuggestions
|
|
63
|
+
selectedStructure={null}
|
|
64
|
+
handleSelectedStructure={jest.fn()}
|
|
65
|
+
selectable={true}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await waitFor(() =>
|
|
70
|
+
expect(mockFetchSuggestions).toHaveBeenCalledWith({
|
|
71
|
+
workflow: "suggest_business_concept_structures_links",
|
|
72
|
+
params: { business_concept_id: "123", prompt: "find structures" },
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("calls fetchSuggestions without prompt when prompt is missing", async () => {
|
|
78
|
+
useLocation.mockReturnValue({ state: null });
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
<StructureSuggestions
|
|
82
|
+
selectedStructure={null}
|
|
83
|
+
handleSelectedStructure={jest.fn()}
|
|
84
|
+
selectable={true}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await waitFor(() =>
|
|
89
|
+
expect(mockFetchSuggestions).toHaveBeenCalledWith({
|
|
90
|
+
workflow: "suggest_business_concept_structures_links",
|
|
91
|
+
params: { business_concept_id: "123", prompt: undefined },
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("shows spinner while waiting for first log item", () => {
|
|
97
|
+
useAgentLayerRun.mockReturnValue({
|
|
98
|
+
trigger: mockFetchSuggestions,
|
|
99
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const rendered = render(
|
|
103
|
+
<StructureSuggestions
|
|
104
|
+
selectedStructure={null}
|
|
105
|
+
handleSelectedStructure={jest.fn()}
|
|
106
|
+
selectable={true}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(rendered.queryByText(/mockthinkingoutloud/i)).not.toBeInTheDocument();
|
|
111
|
+
expect(rendered.queryByText(/mocksearchresults/i)).not.toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("shows ThinkingOutLoud once log items arrive", async () => {
|
|
115
|
+
let capturedOnServerMessage;
|
|
116
|
+
|
|
117
|
+
useAssistantSocket.mockImplementation((_, { onServerMessage }) => {
|
|
118
|
+
capturedOnServerMessage = onServerMessage;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
useAgentLayerRun.mockReturnValue({
|
|
122
|
+
trigger: mockFetchSuggestions,
|
|
123
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const rendered = render(
|
|
127
|
+
<StructureSuggestions
|
|
128
|
+
selectedStructure={null}
|
|
129
|
+
handleSelectedStructure={jest.fn()}
|
|
130
|
+
selectable={true}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
act(() => {
|
|
135
|
+
capturedOnServerMessage("log", {
|
|
136
|
+
priority: 1,
|
|
137
|
+
source: "agent_x",
|
|
138
|
+
content: "Processing...",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await waitFor(() =>
|
|
143
|
+
expect(rendered.queryByText(/mockthinkingoutloud/i)).toBeInTheDocument()
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("disables socket when agent_state_id is not yet available", () => {
|
|
148
|
+
render(
|
|
149
|
+
<StructureSuggestions
|
|
150
|
+
selectedStructure={null}
|
|
151
|
+
handleSelectedStructure={jest.fn()}
|
|
152
|
+
selectable={true}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(useAssistantSocket).toHaveBeenCalledWith(
|
|
157
|
+
true,
|
|
158
|
+
expect.objectContaining({ conversationId: null })
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("connects socket with agent_state_id when available", () => {
|
|
163
|
+
useAgentLayerRun.mockReturnValue({
|
|
164
|
+
trigger: mockFetchSuggestions,
|
|
165
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
render(
|
|
169
|
+
<StructureSuggestions
|
|
170
|
+
selectedStructure={null}
|
|
171
|
+
handleSelectedStructure={jest.fn()}
|
|
172
|
+
selectable={true}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(useAssistantSocket).toHaveBeenCalledWith(
|
|
177
|
+
false,
|
|
178
|
+
expect.objectContaining({ conversationId: "state-1" })
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("shows StructuresSearchResults when stream priority-0 arrives", async () => {
|
|
183
|
+
const structures = [{ id: "s1" }, { id: "s2" }];
|
|
184
|
+
let capturedOnServerMessage;
|
|
185
|
+
|
|
186
|
+
useAssistantSocket.mockImplementation((_, { onServerMessage }) => {
|
|
187
|
+
capturedOnServerMessage = onServerMessage;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
useAgentLayerRun.mockReturnValue({
|
|
191
|
+
trigger: mockFetchSuggestions,
|
|
192
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const rendered = render(
|
|
196
|
+
<StructureSuggestions
|
|
197
|
+
selectedStructure={null}
|
|
198
|
+
handleSelectedStructure={jest.fn()}
|
|
199
|
+
selectable={false}
|
|
200
|
+
/>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
act(() => {
|
|
204
|
+
capturedOnServerMessage("stream", {
|
|
205
|
+
priority: 0,
|
|
206
|
+
content: JSON.stringify({ result: structures }),
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await waitFor(() =>
|
|
211
|
+
expect(rendered.queryByText(/mocksearchresults/i)).toBeInTheDocument()
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(rendered.queryByText(/mockthinkingoutloud/i)).not.toBeInTheDocument();
|
|
215
|
+
|
|
216
|
+
const props = StructuresSearchResults.mock.calls.at(-1)[0];
|
|
217
|
+
expect(props.structures).toEqual(structures);
|
|
218
|
+
expect(props.selectable).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("shows error message when error priority-0 arrives", async () => {
|
|
222
|
+
let capturedOnServerMessage;
|
|
223
|
+
|
|
224
|
+
useAssistantSocket.mockImplementation((_, { onServerMessage }) => {
|
|
225
|
+
capturedOnServerMessage = onServerMessage;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
useAgentLayerRun.mockReturnValue({
|
|
229
|
+
trigger: mockFetchSuggestions,
|
|
230
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const rendered = render(
|
|
234
|
+
<StructureSuggestions
|
|
235
|
+
selectedStructure={null}
|
|
236
|
+
handleSelectedStructure={jest.fn()}
|
|
237
|
+
selectable={true}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
act(() => {
|
|
242
|
+
capturedOnServerMessage("error", {
|
|
243
|
+
priority: 0,
|
|
244
|
+
error: { message: "Something failed" },
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await waitFor(() =>
|
|
249
|
+
expect(rendered.queryByText(/mockthinkingoutloud/i)).not.toBeInTheDocument()
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(rendered.queryByText(/mocksearchresults/i)).not.toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("adds log events to ThinkingOutLoud items", async () => {
|
|
256
|
+
let capturedOnServerMessage;
|
|
257
|
+
|
|
258
|
+
useAssistantSocket.mockImplementation((_, { onServerMessage }) => {
|
|
259
|
+
capturedOnServerMessage = onServerMessage;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
useAgentLayerRun.mockReturnValue({
|
|
263
|
+
trigger: mockFetchSuggestions,
|
|
264
|
+
data: { data: { data: { agent_state_id: "state-1" } } },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
render(
|
|
268
|
+
<StructureSuggestions
|
|
269
|
+
selectedStructure={null}
|
|
270
|
+
handleSelectedStructure={jest.fn()}
|
|
271
|
+
selectable={true}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
act(() => {
|
|
276
|
+
capturedOnServerMessage("log", {
|
|
277
|
+
priority: 1,
|
|
278
|
+
source: "agent_x",
|
|
279
|
+
content: "Processing...",
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await waitFor(() => {
|
|
284
|
+
const lastCall = ThinkingOutLoud.mock.calls.at(-1)[0];
|
|
285
|
+
expect(lastCall.items).toHaveLength(1);
|
|
286
|
+
expect(lastCall.items[0].content).toBe("Processing...");
|
|
287
|
+
expect(lastCall.items[0].eventType).toBe("log");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|