@truedat/ai 8.5.3 → 8.5.6
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/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/components/suggestions/SuggestionsWidget.js +11 -14
- 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
|
@@ -25,6 +25,24 @@ describe("<AssistantChat />", () => {
|
|
|
25
25
|
expect(rendered.container).toMatchSnapshot();
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
it("shows the title in the header when provided", async () => {
|
|
29
|
+
const rendered = render(
|
|
30
|
+
<AssistantChat {...defaultProps} title="My Conversation Title" />
|
|
31
|
+
);
|
|
32
|
+
await waitForLoad(rendered);
|
|
33
|
+
|
|
34
|
+
expect(rendered.getByText("My Conversation Title")).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("shows default new conversation text when title is not provided", async () => {
|
|
38
|
+
const rendered = render(<AssistantChat {...defaultProps} title={null} />);
|
|
39
|
+
await waitForLoad(rendered);
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
rendered.getByText("assistant.newConversation")
|
|
43
|
+
).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
28
46
|
it("groups logs in a thinking block and auto-collapses when a priority-0 response arrives", async () => {
|
|
29
47
|
const conversation = [
|
|
30
48
|
{
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent } from "@testing-library/react";
|
|
3
|
+
import { render, waitForLoad } from "@truedat/test/render";
|
|
4
|
+
|
|
5
|
+
import AssistantConversations from "../AssistantConversations";
|
|
6
|
+
|
|
7
|
+
const conversations = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
updated_at: "2026-04-23T10:00:00Z",
|
|
11
|
+
title: "Data governance overview",
|
|
12
|
+
preview: "How does data governance work?",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 2,
|
|
16
|
+
updated_at: "2026-04-22T08:30:00Z",
|
|
17
|
+
title: null,
|
|
18
|
+
preview: "What is a data domain?",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 3,
|
|
22
|
+
updated_at: "2026-04-21T08:30:00Z",
|
|
23
|
+
title: null,
|
|
24
|
+
preview: null,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const defaultProps = {
|
|
29
|
+
conversations,
|
|
30
|
+
activeId: null,
|
|
31
|
+
onSelect: jest.fn(),
|
|
32
|
+
onNewConversation: jest.fn(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("<AssistantConversations />", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("matches the latest snapshot", async () => {
|
|
41
|
+
const rendered = render(<AssistantConversations {...defaultProps} />);
|
|
42
|
+
await waitForLoad(rendered);
|
|
43
|
+
expect(rendered.container).toMatchSnapshot();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders all conversations", async () => {
|
|
47
|
+
const rendered = render(<AssistantConversations {...defaultProps} />);
|
|
48
|
+
await waitForLoad(rendered);
|
|
49
|
+
|
|
50
|
+
expect(rendered.getByText("Data governance overview")).toBeInTheDocument();
|
|
51
|
+
expect(rendered.getByText("What is a data domain?")).toBeInTheDocument();
|
|
52
|
+
expect(
|
|
53
|
+
rendered.getByText("assistant.conversation.noPreview")
|
|
54
|
+
).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("shows title when available, falling back to preview then placeholder", async () => {
|
|
58
|
+
const rendered = render(<AssistantConversations {...defaultProps} />);
|
|
59
|
+
await waitForLoad(rendered);
|
|
60
|
+
|
|
61
|
+
const items = rendered.container.querySelectorAll(
|
|
62
|
+
".ai-conversation-list__item-preview"
|
|
63
|
+
);
|
|
64
|
+
expect(items[0]).toHaveTextContent("Data governance overview");
|
|
65
|
+
expect(items[1]).toHaveTextContent("What is a data domain?");
|
|
66
|
+
expect(items[2]).toHaveTextContent("assistant.conversation.noPreview");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("marks the active conversation", async () => {
|
|
70
|
+
const rendered = render(
|
|
71
|
+
<AssistantConversations {...defaultProps} activeId={1} />
|
|
72
|
+
);
|
|
73
|
+
await waitForLoad(rendered);
|
|
74
|
+
|
|
75
|
+
const items = rendered.container.querySelectorAll(
|
|
76
|
+
".ai-conversation-list__item"
|
|
77
|
+
);
|
|
78
|
+
expect(items[0]).toHaveClass("ai-conversation-list__item--active");
|
|
79
|
+
expect(items[1]).not.toHaveClass("ai-conversation-list__item--active");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("calls onSelect with conversation id when an item is clicked", async () => {
|
|
83
|
+
const onSelect = jest.fn();
|
|
84
|
+
const rendered = render(
|
|
85
|
+
<AssistantConversations {...defaultProps} onSelect={onSelect} />
|
|
86
|
+
);
|
|
87
|
+
await waitForLoad(rendered);
|
|
88
|
+
|
|
89
|
+
const items = rendered.container.querySelectorAll(
|
|
90
|
+
".ai-conversation-list__item"
|
|
91
|
+
);
|
|
92
|
+
fireEvent.click(items[0]);
|
|
93
|
+
expect(onSelect).toHaveBeenCalledWith(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("calls onNewConversation when the new button is clicked", async () => {
|
|
97
|
+
const onNewConversation = jest.fn();
|
|
98
|
+
const rendered = render(
|
|
99
|
+
<AssistantConversations
|
|
100
|
+
{...defaultProps}
|
|
101
|
+
onNewConversation={onNewConversation}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
await waitForLoad(rendered);
|
|
105
|
+
|
|
106
|
+
fireEvent.click(
|
|
107
|
+
rendered.container.querySelector(".ai-conversation-list__new")
|
|
108
|
+
);
|
|
109
|
+
expect(onNewConversation).toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
exports[`<Assistant /> matches the latest snapshot when socket is ready 1`] = `
|
|
4
4
|
<div>
|
|
5
5
|
<div
|
|
6
|
-
class="assistant-container
|
|
6
|
+
class="assistant-container"
|
|
7
7
|
>
|
|
8
|
-
<
|
|
8
|
+
<button
|
|
9
|
+
aria-expanded="false"
|
|
9
10
|
aria-label="assistant.bubble.label"
|
|
10
11
|
class="assistant-bubble"
|
|
11
|
-
|
|
12
|
-
tabindex="0"
|
|
12
|
+
type="button"
|
|
13
13
|
>
|
|
14
14
|
<i
|
|
15
15
|
aria-hidden="true"
|
|
16
16
|
class="comments icon"
|
|
17
17
|
/>
|
|
18
|
-
</
|
|
18
|
+
</button>
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
21
|
`;
|
|
@@ -6,28 +6,38 @@ exports[`<AssistantChat /> matches the latest snapshot 1`] = `
|
|
|
6
6
|
class="assistant-chat"
|
|
7
7
|
>
|
|
8
8
|
<div
|
|
9
|
-
class="assistant-
|
|
9
|
+
class="assistant-chat__header"
|
|
10
10
|
>
|
|
11
|
+
<span
|
|
12
|
+
class="assistant-chat__header-title"
|
|
13
|
+
>
|
|
14
|
+
assistant.newConversation
|
|
15
|
+
</span>
|
|
11
16
|
<button
|
|
12
|
-
aria-label="assistant.actions.
|
|
13
|
-
class="assistant-chat__action assistant-chat__action--
|
|
17
|
+
aria-label="assistant.actions.close"
|
|
18
|
+
class="assistant-chat__action assistant-chat__action--close"
|
|
14
19
|
type="button"
|
|
15
20
|
>
|
|
16
21
|
<i
|
|
17
22
|
aria-hidden="true"
|
|
18
|
-
class="
|
|
23
|
+
class="close icon"
|
|
19
24
|
/>
|
|
20
|
-
assistant.actions.newChat
|
|
21
25
|
</button>
|
|
26
|
+
</div>
|
|
27
|
+
<div
|
|
28
|
+
class="assistant-chat__actions"
|
|
29
|
+
>
|
|
22
30
|
<button
|
|
23
|
-
aria-label="assistant.actions.
|
|
24
|
-
class="assistant-chat__action
|
|
31
|
+
aria-label="assistant.actions.newChat"
|
|
32
|
+
class="assistant-chat__action"
|
|
33
|
+
title="assistant.actions.newChat"
|
|
25
34
|
type="button"
|
|
26
35
|
>
|
|
27
36
|
<i
|
|
28
37
|
aria-hidden="true"
|
|
29
|
-
class="
|
|
38
|
+
class="comment outline icon"
|
|
30
39
|
/>
|
|
40
|
+
assistant.actions.newChat
|
|
31
41
|
</button>
|
|
32
42
|
</div>
|
|
33
43
|
<div
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`<AssistantConversations /> matches the latest snapshot 1`] = `
|
|
4
|
+
<div>
|
|
5
|
+
<div
|
|
6
|
+
class="ai-conversation-list"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="ai-conversation-list__header"
|
|
10
|
+
>
|
|
11
|
+
<button
|
|
12
|
+
class="ai-conversation-list__new"
|
|
13
|
+
type="button"
|
|
14
|
+
>
|
|
15
|
+
<i
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
class="edit outline icon"
|
|
18
|
+
/>
|
|
19
|
+
assistant.newConversation
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div
|
|
23
|
+
class="ai-conversation-list__items"
|
|
24
|
+
>
|
|
25
|
+
<button
|
|
26
|
+
class="ai-conversation-list__item"
|
|
27
|
+
type="button"
|
|
28
|
+
>
|
|
29
|
+
<span
|
|
30
|
+
class="ai-conversation-list__item-preview"
|
|
31
|
+
>
|
|
32
|
+
Data governance overview
|
|
33
|
+
</span>
|
|
34
|
+
<span
|
|
35
|
+
class="ai-conversation-list__item-date"
|
|
36
|
+
>
|
|
37
|
+
Apr 23, 10:00 AM
|
|
38
|
+
</span>
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
class="ai-conversation-list__item"
|
|
42
|
+
type="button"
|
|
43
|
+
>
|
|
44
|
+
<span
|
|
45
|
+
class="ai-conversation-list__item-preview"
|
|
46
|
+
>
|
|
47
|
+
What is a data domain?
|
|
48
|
+
</span>
|
|
49
|
+
<span
|
|
50
|
+
class="ai-conversation-list__item-date"
|
|
51
|
+
>
|
|
52
|
+
Apr 22, 08:30 AM
|
|
53
|
+
</span>
|
|
54
|
+
</button>
|
|
55
|
+
<button
|
|
56
|
+
class="ai-conversation-list__item"
|
|
57
|
+
type="button"
|
|
58
|
+
>
|
|
59
|
+
<span
|
|
60
|
+
class="ai-conversation-list__item-preview"
|
|
61
|
+
>
|
|
62
|
+
assistant.conversation.noPreview
|
|
63
|
+
</span>
|
|
64
|
+
<span
|
|
65
|
+
class="ai-conversation-list__item-date"
|
|
66
|
+
>
|
|
67
|
+
Apr 21, 08:30 AM
|
|
68
|
+
</span>
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
`;
|
|
@@ -4,8 +4,10 @@ import { apiJsonPost } from "@truedat/core/services/api";
|
|
|
4
4
|
|
|
5
5
|
import { API_AGENT_LAYER_CONVERSATION } from "../../../api";
|
|
6
6
|
|
|
7
|
-
const useAgentConversation = (isOpen) => {
|
|
8
|
-
const [conversationId, setConversationId] = useState(
|
|
7
|
+
const useAgentConversation = (isOpen, initialId = null) => {
|
|
8
|
+
const [conversationId, setConversationId] = useState(
|
|
9
|
+
initialId != null ? String(initialId) : null
|
|
10
|
+
);
|
|
9
11
|
|
|
10
12
|
useEffect(() => {
|
|
11
13
|
if (!isOpen || conversationId != null) return;
|
|
@@ -32,7 +34,11 @@ const useAgentConversation = (isOpen) => {
|
|
|
32
34
|
setConversationId(null);
|
|
33
35
|
}, []);
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
const selectConversation = useCallback((id) => {
|
|
38
|
+
setConversationId(String(id));
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return { conversationId, startNewConversation, selectConversation };
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
export default useAgentConversation;
|
|
@@ -10,7 +10,14 @@ const CONNECT_DELAY_MS = 150;
|
|
|
10
10
|
const RECONNECT_DELAY_MS = 2000;
|
|
11
11
|
|
|
12
12
|
const useAssistantSocket = (disabled = false, options = {}) => {
|
|
13
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
conversationId = null,
|
|
15
|
+
onChannelJoined,
|
|
16
|
+
onServerMessage,
|
|
17
|
+
assistantName = null,
|
|
18
|
+
welcomeMessage = null,
|
|
19
|
+
exact = false,
|
|
20
|
+
} = options;
|
|
14
21
|
const [socketReady, setSocketReady] = useState(false);
|
|
15
22
|
const socketRef = useRef(null);
|
|
16
23
|
const channelRef = useRef(null);
|
|
@@ -65,7 +72,17 @@ const useAssistantSocket = (disabled = false, options = {}) => {
|
|
|
65
72
|
useEffect(() => {
|
|
66
73
|
if (!socketReady || !conversationId || !socketRef.current) return;
|
|
67
74
|
const topic = `agent_layer:${conversationId}`;
|
|
68
|
-
const
|
|
75
|
+
const locale = navigator.language || "en-US";
|
|
76
|
+
const localHour = new Date().getHours();
|
|
77
|
+
const channelParams = {
|
|
78
|
+
locale,
|
|
79
|
+
local_hour: localHour,
|
|
80
|
+
assistant_name: assistantName,
|
|
81
|
+
welcome_message: welcomeMessage,
|
|
82
|
+
exact,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const channel = socketRef.current.channel(topic, channelParams);
|
|
69
86
|
channelRef.current = channel;
|
|
70
87
|
|
|
71
88
|
const handleEvent = (event, payload) => {
|
|
@@ -81,6 +98,12 @@ const useAssistantSocket = (disabled = false, options = {}) => {
|
|
|
81
98
|
const refStream = channel.on("stream", (payload) =>
|
|
82
99
|
handleEvent("stream", payload),
|
|
83
100
|
);
|
|
101
|
+
const refHistory = channel.on("history", (payload) =>
|
|
102
|
+
handleEvent("history", payload),
|
|
103
|
+
);
|
|
104
|
+
const refTitle = channel.on("title", (payload) =>
|
|
105
|
+
handleEvent("title", payload),
|
|
106
|
+
);
|
|
84
107
|
|
|
85
108
|
channel
|
|
86
109
|
.join()
|
|
@@ -93,10 +116,20 @@ const useAssistantSocket = (disabled = false, options = {}) => {
|
|
|
93
116
|
channel.off("log", refLog);
|
|
94
117
|
channel.off("error", refError);
|
|
95
118
|
channel.off("stream", refStream);
|
|
119
|
+
channel.off("history", refHistory);
|
|
120
|
+
channel.off("title", refTitle);
|
|
96
121
|
channelRef.current = null;
|
|
97
122
|
channel.leave();
|
|
98
123
|
};
|
|
99
|
-
}, [
|
|
124
|
+
}, [
|
|
125
|
+
socketReady,
|
|
126
|
+
conversationId,
|
|
127
|
+
onChannelJoined,
|
|
128
|
+
onServerMessage,
|
|
129
|
+
assistantName,
|
|
130
|
+
welcomeMessage,
|
|
131
|
+
exact,
|
|
132
|
+
]);
|
|
100
133
|
|
|
101
134
|
const sendPrompt = useCallback(
|
|
102
135
|
(prompt) => {
|
package/src/components/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AiRoutes from "./AiRoutes";
|
|
2
2
|
import TranslationModal from "./TranslationModal";
|
|
3
3
|
import Assistant from "./assistant/Assistant";
|
|
4
|
+
import AssistantPage from "./assistant/AssistantPage";
|
|
4
5
|
import ThinkingOutLoud, { getSourceLabel } from "./ThinkingOutLoud";
|
|
5
|
-
export { AiRoutes, TranslationModal, Assistant, ThinkingOutLoud, getSourceLabel };
|
|
6
|
+
export { AiRoutes, TranslationModal, Assistant, AssistantPage, ThinkingOutLoud, getSourceLabel };
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
Popup,
|
|
14
14
|
} from "semantic-ui-react";
|
|
15
15
|
import { FormattedMessage } from "react-intl";
|
|
16
|
-
import { toEnrichedTextFormat } from "@truedat/core/services/format";
|
|
17
16
|
|
|
18
17
|
export default function SuggestionsWidget({
|
|
19
18
|
requestAiSuggestion,
|
|
@@ -36,17 +35,17 @@ export default function SuggestionsWidget({
|
|
|
36
35
|
_.map(({ name, fields }) => ({
|
|
37
36
|
name,
|
|
38
37
|
fields: _.filter(fieldInSuggestions)(fields),
|
|
39
|
-
}))
|
|
38
|
+
})),
|
|
40
39
|
)(template);
|
|
41
40
|
const fieldTypes = _.flow(
|
|
42
41
|
_.prop("content"),
|
|
43
42
|
_.flatMap(({ fields }) =>
|
|
44
43
|
_.flow(
|
|
45
44
|
_.filter(({ editable = true }) => editable || !isModification),
|
|
46
|
-
_.map(({ name, type }) => [name, type])
|
|
47
|
-
)(fields)
|
|
45
|
+
_.map(({ name, type }) => [name, type]),
|
|
46
|
+
)(fields),
|
|
48
47
|
),
|
|
49
|
-
_.fromPairs
|
|
48
|
+
_.fromPairs,
|
|
50
49
|
)(template);
|
|
51
50
|
|
|
52
51
|
const validFields = _.keys(fieldTypes);
|
|
@@ -60,7 +59,8 @@ export default function SuggestionsWidget({
|
|
|
60
59
|
requestAiSuggestion(({ data: { data: suggestions } }) => {
|
|
61
60
|
if (_.prop("[0]")(suggestions) === "error") {
|
|
62
61
|
setSuggestionsError(
|
|
63
|
-
_.prop("[1].error.message")(suggestions) ||
|
|
62
|
+
_.prop("[1].error.message")(suggestions) ||
|
|
63
|
+
_.prop("[1]")(suggestions),
|
|
64
64
|
);
|
|
65
65
|
} else {
|
|
66
66
|
setSuggestions(suggestions);
|
|
@@ -68,8 +68,8 @@ export default function SuggestionsWidget({
|
|
|
68
68
|
_.keys,
|
|
69
69
|
_.filter(
|
|
70
70
|
(key) =>
|
|
71
|
-
_.includes(key)(validFields) && !_.isEmpty(suggestions[key])
|
|
72
|
-
)
|
|
71
|
+
_.includes(key)(validFields) && !_.isEmpty(suggestions[key]),
|
|
72
|
+
),
|
|
73
73
|
)(suggestions);
|
|
74
74
|
setSelectedSuggestions(selectedFields);
|
|
75
75
|
}
|
|
@@ -83,15 +83,12 @@ export default function SuggestionsWidget({
|
|
|
83
83
|
_.map(([key, value]) => [
|
|
84
84
|
key,
|
|
85
85
|
{
|
|
86
|
-
value:
|
|
87
|
-
fieldTypes[key] === "enriched_text"
|
|
88
|
-
? toEnrichedTextFormat(value)
|
|
89
|
-
: value,
|
|
86
|
+
value: value,
|
|
90
87
|
origin: "ai",
|
|
91
88
|
},
|
|
92
89
|
]),
|
|
93
90
|
_.fromPairs,
|
|
94
|
-
applySuggestions
|
|
91
|
+
applySuggestions,
|
|
95
92
|
)(suggestions);
|
|
96
93
|
|
|
97
94
|
setSelectedSuggestions([]);
|
|
@@ -102,7 +99,7 @@ export default function SuggestionsWidget({
|
|
|
102
99
|
setSelectedSuggestions(
|
|
103
100
|
_.includes(name)(selectedSuggestions)
|
|
104
101
|
? _.reject((v) => v == name)(selectedSuggestions)
|
|
105
|
-
: [...selectedSuggestions, name]
|
|
102
|
+
: [...selectedSuggestions, name],
|
|
106
103
|
);
|
|
107
104
|
};
|
|
108
105
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import useSWR from "swr";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { apiJson } from "@truedat/core/services/api";
|
|
4
|
+
import { API_AGENT_LAYER_CONVERSATIONS } from "../../api";
|
|
5
|
+
import useAgentConversations from "../useAgentConversations";
|
|
6
|
+
|
|
7
|
+
jest.mock("swr", () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock("@truedat/core/services/api", () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
...jest.requireActual("@truedat/core/services/api"),
|
|
15
|
+
apiJson: jest.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe("useAgentConversations", () => {
|
|
19
|
+
const mockMutate = jest.fn();
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("calls useSWR with the correct endpoint and fetcher", () => {
|
|
26
|
+
useSWR.mockReturnValue({ data: undefined, error: null, mutate: mockMutate });
|
|
27
|
+
renderHook(() => useAgentConversations());
|
|
28
|
+
|
|
29
|
+
expect(useSWR).toHaveBeenCalledWith(API_AGENT_LAYER_CONVERSATIONS, apiJson);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns conversations from nested data.data response", () => {
|
|
33
|
+
const conversations = [{ id: 1, preview: "Hello" }];
|
|
34
|
+
useSWR.mockReturnValue({
|
|
35
|
+
data: { data: { data: conversations } },
|
|
36
|
+
error: null,
|
|
37
|
+
mutate: mockMutate,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const { result } = renderHook(() => useAgentConversations());
|
|
41
|
+
expect(result.current.conversations).toEqual(conversations);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns conversations from flat data response", () => {
|
|
45
|
+
const conversations = [{ id: 2, preview: "World" }];
|
|
46
|
+
useSWR.mockReturnValue({
|
|
47
|
+
data: { data: conversations },
|
|
48
|
+
error: null,
|
|
49
|
+
mutate: mockMutate,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const { result } = renderHook(() => useAgentConversations());
|
|
53
|
+
expect(result.current.conversations).toEqual(conversations);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns empty array when data is undefined", () => {
|
|
57
|
+
useSWR.mockReturnValue({ data: undefined, error: null, mutate: mockMutate });
|
|
58
|
+
|
|
59
|
+
const { result } = renderHook(() => useAgentConversations());
|
|
60
|
+
expect(result.current.conversations).toEqual([]);
|
|
61
|
+
expect(result.current.loading).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns error state when fetch fails", () => {
|
|
65
|
+
const error = new Error("Network error");
|
|
66
|
+
useSWR.mockReturnValue({ data: undefined, error, mutate: mockMutate });
|
|
67
|
+
|
|
68
|
+
const { result } = renderHook(() => useAgentConversations());
|
|
69
|
+
expect(result.current.error).toBe(error);
|
|
70
|
+
expect(result.current.loading).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("exposes refresh as the mutate function", () => {
|
|
74
|
+
useSWR.mockReturnValue({
|
|
75
|
+
data: { data: [] },
|
|
76
|
+
error: null,
|
|
77
|
+
mutate: mockMutate,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const { result } = renderHook(() => useAgentConversations());
|
|
81
|
+
expect(result.current.refresh).toBe(mockMutate);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import useSWR from "swr";
|
|
2
|
+
import { apiJson } from "@truedat/core/services/api";
|
|
3
|
+
import { API_AGENT_LAYER_CONVERSATIONS } from "../api";
|
|
4
|
+
|
|
5
|
+
const useAgentConversations = () => {
|
|
6
|
+
const { data, error, mutate } = useSWR(API_AGENT_LAYER_CONVERSATIONS, apiJson);
|
|
7
|
+
return {
|
|
8
|
+
conversations: data?.data?.data ?? data?.data ?? [],
|
|
9
|
+
loading: !error && !data,
|
|
10
|
+
error,
|
|
11
|
+
refresh: mutate,
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default useAgentConversations;
|