@worktango/ai-assistant 0.0.26 → 0.0.28
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 -7
- package/types.d.ts +18 -2
- package/src/backend/express.test.ts +0 -208
- package/src/backend/express.ts +0 -105
- package/src/frontend/AiAssistant.test.tsx +0 -19
- package/src/frontend/AiAssistant.tsx +0 -26
- package/src/frontend/components/AiAssistantComponent.scss +0 -24
- package/src/frontend/components/AiAssistantComponent.test.tsx +0 -270
- package/src/frontend/components/AiAssistantComponent.tsx +0 -246
- package/src/frontend/components/messageRenderers/AssistantMessage.test.tsx +0 -154
- package/src/frontend/components/messageRenderers/AssistantMessage.tsx +0 -104
- package/src/frontend/components/messageRenderers/UserMessage.test.tsx +0 -103
- package/src/frontend/components/messageRenderers/UserMessage.tsx +0 -43
- package/src/frontend/config.ts +0 -54
- package/src/frontend/reactComponent.ts +0 -6
- package/src/frontend/tools/generatedSdks.ts +0 -44
- package/src/frontend/tools/llm-tools/getCurrentUser.test.ts +0 -100
- package/src/frontend/tools/llm-tools/getCurrentUser.ts +0 -158
- package/src/frontend/tools/llm-tools/index.ts +0 -6
- package/src/frontend/tools/llm-tools/kazooPlatform/getRewardsAndRecognitionCurrentUser.test.ts +0 -76
- package/src/frontend/tools/llm-tools/kazooPlatform/getRewardsAndRecognitionCurrentUser.ts +0 -36
- package/src/frontend/tools/llm-tools/pulsePlatform/.gitkeep +0 -0
- package/src/frontend/tools/llm-tools/shared/.gitkeep +0 -0
- package/src/frontend/tools/useAiAssistantSession.test.ts +0 -768
- package/src/frontend/tools/useAiAssistantSession.ts +0 -374
- package/src/frontend/useStyles.ts +0 -36
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
import { ToastProvider } from "@kazoohr/confetti";
|
|
4
|
-
import { act, fireEvent, render, screen } from "@kazoohr/test";
|
|
5
|
-
|
|
6
|
-
import { useAiAssistantSession } from "../tools/useAiAssistantSession";
|
|
7
|
-
|
|
8
|
-
import { AiAssistantComponent } from "./AiAssistantComponent";
|
|
9
|
-
|
|
10
|
-
jest.mock("../tools/useAiAssistantSession", () => ({
|
|
11
|
-
useAiAssistantSession: jest.fn(),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
// Need to mock micromark because it uses ES modules
|
|
15
|
-
jest.mock("micromark", () => ({
|
|
16
|
-
micromark: (text: string) => `<div>${text}</div>`,
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
describe("AiAssistantComponent", () => {
|
|
20
|
-
const mockSendUserMessage = jest.fn();
|
|
21
|
-
const mockSetMessages = jest.fn();
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
jest.clearAllMocks();
|
|
25
|
-
jest.useFakeTimers();
|
|
26
|
-
|
|
27
|
-
(useAiAssistantSession as jest.Mock).mockReturnValue({
|
|
28
|
-
messages: [],
|
|
29
|
-
sendUserMessage: mockSendUserMessage,
|
|
30
|
-
isLoading: false,
|
|
31
|
-
debugInfo: {},
|
|
32
|
-
setMessages: mockSetMessages,
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
jest.useRealTimers();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const renderWithToast = (ui: React.ReactNode) => {
|
|
41
|
-
return render(<ToastProvider>{ui}</ToastProvider>);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
it("renders initial empty state", () => {
|
|
45
|
-
renderWithToast(<AiAssistantComponent />);
|
|
46
|
-
|
|
47
|
-
expect(
|
|
48
|
-
screen.getByText(
|
|
49
|
-
"No messages yet! Get started by asking a question below."
|
|
50
|
-
)
|
|
51
|
-
).toBeInTheDocument();
|
|
52
|
-
expect(screen.getByText("Send")).toBeInTheDocument();
|
|
53
|
-
expect(screen.getByText("Clear current chat")).toBeInTheDocument();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("handles message sending", async () => {
|
|
57
|
-
renderWithToast(<AiAssistantComponent />);
|
|
58
|
-
|
|
59
|
-
const textarea = screen.getByRole("textbox");
|
|
60
|
-
const sendButton = screen.getByText("Send");
|
|
61
|
-
|
|
62
|
-
// Clear the default text first
|
|
63
|
-
fireEvent.change(textarea, { target: { value: "" } });
|
|
64
|
-
expect(sendButton).toBeDisabled();
|
|
65
|
-
|
|
66
|
-
fireEvent.change(textarea, { target: { value: "This is a test message" } });
|
|
67
|
-
expect(sendButton).not.toBeDisabled();
|
|
68
|
-
|
|
69
|
-
// Send the message
|
|
70
|
-
fireEvent.click(sendButton);
|
|
71
|
-
expect(mockSendUserMessage).toHaveBeenCalledWith("This is a test message");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("handles ctrl+enter submission", () => {
|
|
75
|
-
renderWithToast(<AiAssistantComponent />);
|
|
76
|
-
|
|
77
|
-
const textarea = screen.getByRole("textbox");
|
|
78
|
-
const sendButton = screen.getByText("Send");
|
|
79
|
-
|
|
80
|
-
fireEvent.change(textarea, { target: { value: "This is a test message" } });
|
|
81
|
-
expect(sendButton).not.toBeDisabled();
|
|
82
|
-
|
|
83
|
-
fireEvent.keyDown(textarea, { key: "Space", ctrlKey: false });
|
|
84
|
-
|
|
85
|
-
expect(mockSendUserMessage).not.toHaveBeenCalled();
|
|
86
|
-
|
|
87
|
-
fireEvent.keyDown(textarea, { key: "Enter", ctrlKey: true });
|
|
88
|
-
|
|
89
|
-
expect(mockSendUserMessage).toHaveBeenCalledWith("This is a test message");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("displays loading state", () => {
|
|
93
|
-
(useAiAssistantSession as jest.Mock).mockReturnValue({
|
|
94
|
-
messages: [
|
|
95
|
-
{
|
|
96
|
-
role: "user",
|
|
97
|
-
parts: [{ type: "text", text: "Hello" }],
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
sendUserMessage: mockSendUserMessage,
|
|
101
|
-
isLoading: true,
|
|
102
|
-
debugInfo: {},
|
|
103
|
-
setMessages: mockSetMessages,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const { baseElement } = renderWithToast(<AiAssistantComponent />);
|
|
107
|
-
|
|
108
|
-
expect(baseElement.innerHTML).toContain("loading");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("display the LLM loading state", () => {
|
|
112
|
-
(useAiAssistantSession as jest.Mock).mockReturnValue({
|
|
113
|
-
messages: [
|
|
114
|
-
{
|
|
115
|
-
role: "user",
|
|
116
|
-
parts: [{ type: "text", text: "Hello" }],
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
sendUserMessage: mockSendUserMessage,
|
|
120
|
-
isLoading: true,
|
|
121
|
-
debugInfo: {},
|
|
122
|
-
setMessages: mockSetMessages,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
renderWithToast(<AiAssistantComponent />);
|
|
126
|
-
|
|
127
|
-
expect(screen.getByText("Thinking...")).toBeInTheDocument();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("displays messages correctly", () => {
|
|
131
|
-
(useAiAssistantSession as jest.Mock).mockReturnValue({
|
|
132
|
-
messages: [
|
|
133
|
-
{
|
|
134
|
-
role: "user",
|
|
135
|
-
parts: [{ type: "text", text: "Hello" }],
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
role: "assistant",
|
|
139
|
-
parts: [{ type: "text", text: "Hi there!" }],
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
role: "assistant",
|
|
143
|
-
parts: [
|
|
144
|
-
{ type: "toolCall", toolName: "testTool", args: { test: true } },
|
|
145
|
-
{
|
|
146
|
-
type: "toolResult",
|
|
147
|
-
toolName: "testTool",
|
|
148
|
-
result: { success: true },
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
role: "tool",
|
|
154
|
-
parts: [
|
|
155
|
-
{
|
|
156
|
-
type: "UNKNOWN!",
|
|
157
|
-
toolName: "testTool",
|
|
158
|
-
result: { success: true },
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
sendUserMessage: mockSendUserMessage,
|
|
164
|
-
isLoading: false,
|
|
165
|
-
debugInfo: {},
|
|
166
|
-
setMessages: mockSetMessages,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const { baseElement } = renderWithToast(<AiAssistantComponent />);
|
|
170
|
-
const html = baseElement.innerHTML;
|
|
171
|
-
|
|
172
|
-
expect(html).toContain("Hi there!");
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("handles debug mode toggle", () => {
|
|
176
|
-
(useAiAssistantSession as jest.Mock).mockReturnValue({
|
|
177
|
-
messages: [
|
|
178
|
-
{ role: "user", parts: [{ type: "text", text: "Hello world!" }] },
|
|
179
|
-
],
|
|
180
|
-
sendUserMessage: mockSendUserMessage,
|
|
181
|
-
isLoading: false,
|
|
182
|
-
debugInfo: { test: true },
|
|
183
|
-
setMessages: mockSetMessages,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
renderWithToast(<AiAssistantComponent />);
|
|
187
|
-
|
|
188
|
-
// Initially debug info is hidden
|
|
189
|
-
const debugButton = screen.getByTitle("Show debug info");
|
|
190
|
-
expect(debugButton).toBeInTheDocument();
|
|
191
|
-
|
|
192
|
-
// Show debug info
|
|
193
|
-
fireEvent.click(debugButton);
|
|
194
|
-
|
|
195
|
-
// Find the tabs
|
|
196
|
-
const tabs = screen.getAllByRole("tab");
|
|
197
|
-
expect(tabs[0]).toHaveTextContent("History");
|
|
198
|
-
expect(tabs[1]).toHaveTextContent("Debug");
|
|
199
|
-
|
|
200
|
-
// Click the messages tab
|
|
201
|
-
fireEvent.click(tabs[0]);
|
|
202
|
-
|
|
203
|
-
expect(screen.getByText("Hello world!")).toBeInTheDocument();
|
|
204
|
-
|
|
205
|
-
// Click the debug tab
|
|
206
|
-
fireEvent.click(tabs[1]);
|
|
207
|
-
|
|
208
|
-
// Check that the debug info is displayed
|
|
209
|
-
expect(screen.getByTestId("debug-textarea")).toHaveTextContent("test");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("handles expand/collapse functionality", () => {
|
|
213
|
-
(useAiAssistantSession as jest.Mock).mockReturnValue({
|
|
214
|
-
messages: [{ role: "user", parts: [{ type: "text", text: "test" }] }],
|
|
215
|
-
sendUserMessage: mockSendUserMessage,
|
|
216
|
-
isLoading: false,
|
|
217
|
-
debugInfo: {},
|
|
218
|
-
setMessages: mockSetMessages,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
renderWithToast(<AiAssistantComponent />);
|
|
222
|
-
|
|
223
|
-
const collapseButton = screen.getByTitle("Collapse chat window");
|
|
224
|
-
expect(collapseButton).toBeInTheDocument();
|
|
225
|
-
fireEvent.click(collapseButton);
|
|
226
|
-
|
|
227
|
-
const expandButton = screen.getByTitle("Expand chat window");
|
|
228
|
-
expect(expandButton).toBeInTheDocument();
|
|
229
|
-
fireEvent.click(expandButton);
|
|
230
|
-
|
|
231
|
-
expect(screen.getByTitle("Collapse chat window")).toBeInTheDocument();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("handles clearing chat", () => {
|
|
235
|
-
renderWithToast(<AiAssistantComponent />);
|
|
236
|
-
|
|
237
|
-
fireEvent.click(screen.getByText("Clear current chat"));
|
|
238
|
-
expect(mockSetMessages).toHaveBeenCalledWith([]);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("handles errors", () => {
|
|
242
|
-
renderWithToast(<AiAssistantComponent />);
|
|
243
|
-
|
|
244
|
-
const error = new Error("Test error");
|
|
245
|
-
const { onError } = (useAiAssistantSession as jest.Mock).mock.calls[0][0];
|
|
246
|
-
|
|
247
|
-
act(() => {
|
|
248
|
-
onError(error);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
expect(screen.getByText("Test error")).toBeInTheDocument();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("handles scroll to bottom on message send", () => {
|
|
255
|
-
const mockRef = { current: { scrollTop: 0, scrollHeight: 100 } };
|
|
256
|
-
jest.spyOn(React, "useRef").mockReturnValue(mockRef);
|
|
257
|
-
|
|
258
|
-
renderWithToast(<AiAssistantComponent />);
|
|
259
|
-
|
|
260
|
-
const { onBeforeLlmCall } = (useAiAssistantSession as jest.Mock).mock
|
|
261
|
-
.calls[0][0];
|
|
262
|
-
|
|
263
|
-
act(() => {
|
|
264
|
-
onBeforeLlmCall();
|
|
265
|
-
jest.advanceTimersByTime(10); // Advance past the setTimeout
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
expect(mockRef.current.scrollTop).toBe(mockRef.current.scrollHeight);
|
|
269
|
-
});
|
|
270
|
-
});
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
BRANDING_CLASSES,
|
|
5
|
-
Button,
|
|
6
|
-
Card,
|
|
7
|
-
Flex,
|
|
8
|
-
Illustration,
|
|
9
|
-
Loader,
|
|
10
|
-
PageHeader,
|
|
11
|
-
Spacer,
|
|
12
|
-
Tab,
|
|
13
|
-
Tabs,
|
|
14
|
-
Text,
|
|
15
|
-
TextArea,
|
|
16
|
-
useToast,
|
|
17
|
-
} from "@kazoohr/confetti";
|
|
18
|
-
import {
|
|
19
|
-
composeSimpleFunctions,
|
|
20
|
-
getClosureFor,
|
|
21
|
-
setState,
|
|
22
|
-
setStateValue,
|
|
23
|
-
} from "@kazoohr/helpers";
|
|
24
|
-
import { ToolDeclaration } from "@kazoohr/llm/types";
|
|
25
|
-
|
|
26
|
-
import { useAiAssistantSession } from "../tools/useAiAssistantSession";
|
|
27
|
-
|
|
28
|
-
import styles from "./AiAssistantComponent.scss";
|
|
29
|
-
import { AssistantMessage } from "./messageRenderers/AssistantMessage";
|
|
30
|
-
import { UserMessage } from "./messageRenderers/UserMessage";
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Our AI Assistant component, for managing the LLM session and displaying a
|
|
34
|
-
* single chat session with the LLM.
|
|
35
|
-
*/
|
|
36
|
-
// eslint-disable-next-line complexity
|
|
37
|
-
export function AiAssistantComponent({ tools }: { tools?: ToolDeclaration[] }) {
|
|
38
|
-
const { error: errorToast } = useToast();
|
|
39
|
-
const [showDebugInfo, setShowDebugInfo] = React.useState(false);
|
|
40
|
-
const [prompt, setPrompt] = React.useState(
|
|
41
|
-
'This is a test! Give me a "Hello world! From <your model>". Then, use your tool calls to tell me about myself. Be comprehensive in your answer.'
|
|
42
|
-
);
|
|
43
|
-
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
44
|
-
const scrollWindowRef = React.useRef<HTMLDivElement>(null);
|
|
45
|
-
const scrollWindowToBottom = React.useCallback(
|
|
46
|
-
// istanbul ignore next - UI-related stuff
|
|
47
|
-
() => {
|
|
48
|
-
setTimeout(() => {
|
|
49
|
-
if (scrollWindowRef.current) {
|
|
50
|
-
scrollWindowRef.current.scrollTop =
|
|
51
|
-
scrollWindowRef.current.scrollHeight;
|
|
52
|
-
}
|
|
53
|
-
}, 10);
|
|
54
|
-
},
|
|
55
|
-
[]
|
|
56
|
-
);
|
|
57
|
-
const { messages, sendUserMessage, isLoading, debugInfo, setMessages } =
|
|
58
|
-
useAiAssistantSession({
|
|
59
|
-
onBeforeLlmCall: scrollWindowToBottom,
|
|
60
|
-
onAfterLlmCall: composeSimpleFunctions(
|
|
61
|
-
setStateValue(setPrompt, ""),
|
|
62
|
-
scrollWindowToBottom,
|
|
63
|
-
textareaRef.current?.focus
|
|
64
|
-
),
|
|
65
|
-
onError: (error) => errorToast(error.message),
|
|
66
|
-
onChunkAdded: scrollWindowToBottom,
|
|
67
|
-
tools,
|
|
68
|
-
});
|
|
69
|
-
const [fullHeight, setFullHeight] = React.useState(true);
|
|
70
|
-
const [tab, setTab] = React.useState<"messages" | "debug">("messages");
|
|
71
|
-
React.useEffect(() => {
|
|
72
|
-
// istanbul ignore else - noop
|
|
73
|
-
if (textareaRef.current) {
|
|
74
|
-
// Ctrl + enter submits the form
|
|
75
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
76
|
-
if (event.ctrlKey && event.key === "Enter") {
|
|
77
|
-
event.preventDefault();
|
|
78
|
-
sendUserMessage(prompt);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
textareaRef.current.addEventListener("keydown", handleKeyDown);
|
|
82
|
-
return () => {
|
|
83
|
-
textareaRef.current?.removeEventListener("keydown", handleKeyDown);
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
}, [prompt, sendUserMessage]);
|
|
87
|
-
|
|
88
|
-
const canSubmit = prompt.trim().length > 5;
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<div>
|
|
92
|
-
<style type="text/css">
|
|
93
|
-
{`
|
|
94
|
-
.streaming-text-resizing-container {
|
|
95
|
-
overflow-y: hidden;
|
|
96
|
-
transition: height 0.1s ease-in-out;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.markdown {
|
|
100
|
-
line-height: 1.4;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.markdown * + p {
|
|
104
|
-
margin-top: 12px;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.markdown ul, .markdown ol {
|
|
108
|
-
margin-top: 12px;
|
|
109
|
-
margin-left: 24px;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.scroll-window {
|
|
113
|
-
padding: 12px 4px 16px 0;
|
|
114
|
-
overflow-y: scroll;
|
|
115
|
-
overflow-x: hidden;
|
|
116
|
-
height: min(100vh, max(50vh, 400px));
|
|
117
|
-
}
|
|
118
|
-
`}
|
|
119
|
-
</style>
|
|
120
|
-
<PageHeader
|
|
121
|
-
header="AI Assistant"
|
|
122
|
-
withBottomBorder={false}
|
|
123
|
-
renderRightContent={() => (
|
|
124
|
-
<Flex gap="12px">
|
|
125
|
-
<Button
|
|
126
|
-
icon={fullHeight ? "caret_up" : "caret_dn"}
|
|
127
|
-
onClick={setStateValue(setFullHeight, !fullHeight)}
|
|
128
|
-
title={fullHeight ? "Collapse chat window" : "Expand chat window"}
|
|
129
|
-
variant="transparent"
|
|
130
|
-
size="extra_small"
|
|
131
|
-
>
|
|
132
|
-
{fullHeight ? "Collapse" : "Expand"}
|
|
133
|
-
</Button>
|
|
134
|
-
<Button
|
|
135
|
-
icon="eye"
|
|
136
|
-
onClick={composeSimpleFunctions(
|
|
137
|
-
setStateValue(setShowDebugInfo, !showDebugInfo),
|
|
138
|
-
setStateValue(setTab, "messages")
|
|
139
|
-
)}
|
|
140
|
-
title={showDebugInfo ? "Hide debug info" : "Show debug info"}
|
|
141
|
-
variant="transparent"
|
|
142
|
-
size="extra_small"
|
|
143
|
-
>
|
|
144
|
-
{showDebugInfo ? "Debugging" : "Debug"}
|
|
145
|
-
</Button>
|
|
146
|
-
<Button
|
|
147
|
-
icon="close"
|
|
148
|
-
variant="transparent"
|
|
149
|
-
onClick={setStateValue(setMessages, [])}
|
|
150
|
-
size="extra_small"
|
|
151
|
-
>
|
|
152
|
-
Clear current chat
|
|
153
|
-
</Button>
|
|
154
|
-
</Flex>
|
|
155
|
-
)}
|
|
156
|
-
/>
|
|
157
|
-
{showDebugInfo && (
|
|
158
|
-
<>
|
|
159
|
-
<Tabs value={tab} onChange={setState(setTab)}>
|
|
160
|
-
<Tab value="messages" data-testid="messages-tab">
|
|
161
|
-
History
|
|
162
|
-
</Tab>
|
|
163
|
-
<Tab value="debug" data-testid="debug-tab">
|
|
164
|
-
Debug
|
|
165
|
-
</Tab>
|
|
166
|
-
</Tabs>
|
|
167
|
-
<Spacer />
|
|
168
|
-
</>
|
|
169
|
-
)}
|
|
170
|
-
{tab === "debug" && (
|
|
171
|
-
<Card>
|
|
172
|
-
<textarea
|
|
173
|
-
value={JSON.stringify(debugInfo, null, 2)}
|
|
174
|
-
className={styles["ai-assistant-debug-textarea"]}
|
|
175
|
-
readOnly
|
|
176
|
-
data-testid="debug-textarea"
|
|
177
|
-
/>
|
|
178
|
-
</Card>
|
|
179
|
-
)}
|
|
180
|
-
{tab === "messages" &&
|
|
181
|
-
(messages.length === 0 ? (
|
|
182
|
-
<Flex direction="column" alignItems="center" gap="12px">
|
|
183
|
-
<Illustration illustration="feedback" size={196} />
|
|
184
|
-
<Text slim size="large">
|
|
185
|
-
No messages yet! Get started by asking a question below.
|
|
186
|
-
</Text>
|
|
187
|
-
</Flex>
|
|
188
|
-
) : (
|
|
189
|
-
<>
|
|
190
|
-
<div
|
|
191
|
-
className={fullHeight ? "" : "scroll-window"}
|
|
192
|
-
ref={scrollWindowRef}
|
|
193
|
-
>
|
|
194
|
-
<Flex direction="column" gap="16px" justifyContent="flex-end">
|
|
195
|
-
{messages.map((message, index) => {
|
|
196
|
-
if (message.role === "user") {
|
|
197
|
-
return <UserMessage key={index} message={message} />;
|
|
198
|
-
}
|
|
199
|
-
return <AssistantMessage key={index} message={message} />;
|
|
200
|
-
})}
|
|
201
|
-
{isLoading && (
|
|
202
|
-
<div style={{ height: "100px" }}>
|
|
203
|
-
<Loader />
|
|
204
|
-
</div>
|
|
205
|
-
)}
|
|
206
|
-
</Flex>
|
|
207
|
-
</div>
|
|
208
|
-
<Spacer size="large" />
|
|
209
|
-
</>
|
|
210
|
-
))}
|
|
211
|
-
<Spacer />
|
|
212
|
-
<Card
|
|
213
|
-
className={BRANDING_CLASSES.PRIMARY_COLOR_BACKGROUND}
|
|
214
|
-
background="two-tone"
|
|
215
|
-
data-testid="toggle-panel-controls"
|
|
216
|
-
>
|
|
217
|
-
<TextArea
|
|
218
|
-
id="id"
|
|
219
|
-
value={prompt}
|
|
220
|
-
onChange={setState(setPrompt)}
|
|
221
|
-
fullWidth
|
|
222
|
-
className={styles["ai-assistant-textarea"]}
|
|
223
|
-
placeholder="Ask a question..."
|
|
224
|
-
inputRef={textareaRef}
|
|
225
|
-
autoFocus
|
|
226
|
-
disabled={isLoading}
|
|
227
|
-
/>
|
|
228
|
-
<Spacer size="small" />
|
|
229
|
-
<Flex justifyContent="flex-end" gap="8px">
|
|
230
|
-
<Button
|
|
231
|
-
onClick={getClosureFor(sendUserMessage, prompt)}
|
|
232
|
-
disabled={!canSubmit || isLoading}
|
|
233
|
-
variant="feature_inverted"
|
|
234
|
-
size="large"
|
|
235
|
-
icon="people"
|
|
236
|
-
style={{
|
|
237
|
-
cursor: !canSubmit || isLoading ? "not-allowed" : "pointer",
|
|
238
|
-
}}
|
|
239
|
-
>
|
|
240
|
-
{isLoading ? "Thinking..." : "Send"}
|
|
241
|
-
</Button>
|
|
242
|
-
</Flex>
|
|
243
|
-
</Card>
|
|
244
|
-
</div>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
import { render, screen } from "@kazoohr/test";
|
|
4
|
-
|
|
5
|
-
import { type ChatItemWithTimestamp } from "../../tools/useAiAssistantSession";
|
|
6
|
-
|
|
7
|
-
import { AssistantMessage } from "./AssistantMessage";
|
|
8
|
-
|
|
9
|
-
jest.mock("micromark", () => ({
|
|
10
|
-
micromark: (text: string) => text,
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
describe("AssistantMessage", () => {
|
|
14
|
-
const mockTimestamp = new Date("2024-01-01T12:00:00Z");
|
|
15
|
-
|
|
16
|
-
it("renders text message correctly", () => {
|
|
17
|
-
const message: ChatItemWithTimestamp = {
|
|
18
|
-
role: "model",
|
|
19
|
-
timestamp: mockTimestamp,
|
|
20
|
-
parts: [
|
|
21
|
-
{
|
|
22
|
-
type: "text",
|
|
23
|
-
text: "Hello world",
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
render(<AssistantMessage message={message} />);
|
|
29
|
-
|
|
30
|
-
expect(screen.getByText("assistant")).toBeInTheDocument();
|
|
31
|
-
expect(screen.getByText("Hello world")).toBeInTheDocument();
|
|
32
|
-
expect(
|
|
33
|
-
screen.getByText(mockTimestamp.toLocaleString())
|
|
34
|
-
).toBeInTheDocument();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("renders markdown text correctly", () => {
|
|
38
|
-
const message: ChatItemWithTimestamp = {
|
|
39
|
-
role: "model",
|
|
40
|
-
timestamp: mockTimestamp,
|
|
41
|
-
parts: [
|
|
42
|
-
{
|
|
43
|
-
type: "text",
|
|
44
|
-
text: "# Hello\n\n**Bold text**",
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
render(<AssistantMessage message={message} />);
|
|
50
|
-
|
|
51
|
-
const content = document.querySelector(".markdown");
|
|
52
|
-
expect(content).toBeInTheDocument();
|
|
53
|
-
expect(content?.innerHTML).toContain("# Hello\n\n**Bold text**");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("renders tool calls correctly", () => {
|
|
57
|
-
const message: ChatItemWithTimestamp = {
|
|
58
|
-
role: "model",
|
|
59
|
-
timestamp: mockTimestamp,
|
|
60
|
-
parts: [
|
|
61
|
-
{
|
|
62
|
-
type: "toolCall",
|
|
63
|
-
toolName: "testTool",
|
|
64
|
-
args: { test: true },
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
render(<AssistantMessage message={message} />);
|
|
70
|
-
|
|
71
|
-
expect(screen.getByTitle("Tool call: testTool")).toBeInTheDocument();
|
|
72
|
-
expect(screen.getByText("Fetch data")).toBeInTheDocument();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("renders both text and tool calls together", () => {
|
|
76
|
-
const message: ChatItemWithTimestamp = {
|
|
77
|
-
role: "model",
|
|
78
|
-
timestamp: mockTimestamp,
|
|
79
|
-
parts: [
|
|
80
|
-
{
|
|
81
|
-
type: "text",
|
|
82
|
-
text: "Hello world",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
type: "toolCall",
|
|
86
|
-
toolName: "testTool",
|
|
87
|
-
args: { test: true },
|
|
88
|
-
},
|
|
89
|
-
],
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
render(<AssistantMessage message={message} />);
|
|
93
|
-
|
|
94
|
-
expect(screen.getByText("Hello world")).toBeInTheDocument();
|
|
95
|
-
expect(screen.getByTitle("Tool call: testTool")).toBeInTheDocument();
|
|
96
|
-
expect(screen.getByText("Fetch data")).toBeInTheDocument();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("renders multiple tool calls", () => {
|
|
100
|
-
const message: ChatItemWithTimestamp = {
|
|
101
|
-
role: "model",
|
|
102
|
-
timestamp: mockTimestamp,
|
|
103
|
-
parts: [
|
|
104
|
-
{
|
|
105
|
-
type: "toolCall",
|
|
106
|
-
toolName: "tool1",
|
|
107
|
-
args: { test: true },
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
type: "toolCall",
|
|
111
|
-
toolName: "tool2",
|
|
112
|
-
args: { test: true },
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
render(<AssistantMessage message={message} />);
|
|
118
|
-
|
|
119
|
-
expect(screen.getByTitle("Tool call: tool1")).toBeInTheDocument();
|
|
120
|
-
expect(screen.getByTitle("Tool call: tool2")).toBeInTheDocument();
|
|
121
|
-
expect(screen.getAllByText("Fetch data")).toHaveLength(2);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("returns null for empty message parts", () => {
|
|
125
|
-
const message: ChatItemWithTimestamp = {
|
|
126
|
-
role: "model",
|
|
127
|
-
timestamp: mockTimestamp,
|
|
128
|
-
parts: [],
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const { container } = render(<AssistantMessage message={message} />);
|
|
132
|
-
expect(container.firstChild).toBeNull();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("aggregates multiple text parts", () => {
|
|
136
|
-
const message: ChatItemWithTimestamp = {
|
|
137
|
-
role: "model",
|
|
138
|
-
timestamp: mockTimestamp,
|
|
139
|
-
parts: [
|
|
140
|
-
{
|
|
141
|
-
type: "text",
|
|
142
|
-
text: "Hello",
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
type: "text",
|
|
146
|
-
text: " world",
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
render(<AssistantMessage message={message} />);
|
|
152
|
-
expect(screen.getByText("Hello world")).toBeInTheDocument();
|
|
153
|
-
});
|
|
154
|
-
});
|