@truedat/ai 8.5.4 → 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.
@@ -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 assistant-container--closed"
6
+ class="assistant-container"
7
7
  >
8
- <div
8
+ <button
9
+ aria-expanded="false"
9
10
  aria-label="assistant.bubble.label"
10
11
  class="assistant-bubble"
11
- role="button"
12
- tabindex="0"
12
+ type="button"
13
13
  >
14
14
  <i
15
15
  aria-hidden="true"
16
16
  class="comments icon"
17
17
  />
18
- </div>
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-chat__actions"
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.newChat"
13
- class="assistant-chat__action assistant-chat__action--new-chat"
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="comment outline icon"
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.close"
24
- class="assistant-chat__action assistant-chat__action--close"
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="close icon"
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(null);
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
- return { conversationId, startNewConversation };
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 { conversationId = null, onChannelJoined, onServerMessage } = options;
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 channel = socketRef.current.channel(topic, {});
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
- }, [socketReady, conversationId, onChannelJoined, onServerMessage]);
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) => {
@@ -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 };
@@ -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;