@eeacms/volto-eea-chatbot 2.0.0 → 2.0.2

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.
Files changed (81) hide show
  1. package/.eslintrc.js +6 -6
  2. package/CHANGELOG.md +12 -1
  3. package/jest-addon.config.js +1 -0
  4. package/package.json +1 -1
  5. package/src/ChatBlock/ChatBlockEdit.jsx +2 -1
  6. package/src/ChatBlock/chat/AIMessage.tsx +20 -16
  7. package/src/ChatBlock/chat/ChatMessage.tsx +1 -1
  8. package/src/ChatBlock/chat/ChatWindow.tsx +10 -11
  9. package/src/ChatBlock/chat/UserMessage.tsx +4 -4
  10. package/src/ChatBlock/components/AutoResizeTextarea.jsx +1 -1
  11. package/src/ChatBlock/components/ChatMessageFeedback.jsx +2 -2
  12. package/src/ChatBlock/components/EmptyState.jsx +1 -1
  13. package/src/ChatBlock/components/FeedbackModal.jsx +1 -1
  14. package/src/ChatBlock/components/HalloumiFeedback.jsx +2 -2
  15. package/src/ChatBlock/components/Source.jsx +2 -2
  16. package/src/ChatBlock/components/UserActionsToolbar.jsx +3 -3
  17. package/src/ChatBlock/components/WebResultIcon.tsx +2 -2
  18. package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
  19. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +4 -4
  20. package/src/ChatBlock/components/markdown/{index.js → index.jsx} +1 -1
  21. package/src/ChatBlock/hooks/useChatController.ts +7 -4
  22. package/src/ChatBlock/hooks/useChatStreaming.ts +4 -4
  23. package/src/ChatBlock/hooks/useToolDisplayTiming.ts +1 -1
  24. package/src/ChatBlock/index.js +8 -0
  25. package/src/ChatBlock/packets/MultiToolRenderer.tsx +11 -12
  26. package/src/ChatBlock/packets/RendererComponent.tsx +6 -3
  27. package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +3 -3
  28. package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +3 -3
  29. package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +3 -3
  30. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -8
  31. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +5 -5
  32. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +10 -10
  33. package/src/ChatBlock/services/messageProcessor.ts +6 -3
  34. package/src/ChatBlock/services/packetUtils.ts +2 -2
  35. package/src/ChatBlock/services/streamingService.ts +8 -2
  36. package/src/ChatBlock/utils/citations.ts +1 -1
  37. package/src/halloumi/filtering.test.js +199 -1
  38. package/src/ChatBlock/tests/AIMessage.test.jsx +0 -95
  39. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +0 -49
  40. package/src/ChatBlock/tests/BlinkingDot.test.jsx +0 -71
  41. package/src/ChatBlock/tests/ChatMessage.test.jsx +0 -75
  42. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +0 -73
  43. package/src/ChatBlock/tests/Citation.test.jsx +0 -107
  44. package/src/ChatBlock/tests/ClaimModal.test.jsx +0 -136
  45. package/src/ChatBlock/tests/ClaimSegments.test.jsx +0 -206
  46. package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +0 -241
  47. package/src/ChatBlock/tests/EmptyState.test.jsx +0 -137
  48. package/src/ChatBlock/tests/FeedbackModal.test.jsx +0 -138
  49. package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +0 -161
  50. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +0 -94
  51. package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +0 -178
  52. package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +0 -227
  53. package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +0 -134
  54. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +0 -105
  55. package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +0 -163
  56. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +0 -215
  57. package/src/ChatBlock/tests/RenderClaimView.test.jsx +0 -191
  58. package/src/ChatBlock/tests/RendererComponent.test.jsx +0 -139
  59. package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +0 -295
  60. package/src/ChatBlock/tests/Source.test.jsx +0 -79
  61. package/src/ChatBlock/tests/SourceChip.test.jsx +0 -108
  62. package/src/ChatBlock/tests/Spinner.test.jsx +0 -18
  63. package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +0 -135
  64. package/src/ChatBlock/tests/UserMessage.test.jsx +0 -83
  65. package/src/ChatBlock/tests/WebResultIcon.test.jsx +0 -61
  66. package/src/ChatBlock/tests/citations.test.js +0 -114
  67. package/src/ChatBlock/tests/index.test.js +0 -51
  68. package/src/ChatBlock/tests/messageProcessor.test.jsx +0 -438
  69. package/src/ChatBlock/tests/packetUtils.test.js +0 -158
  70. package/src/ChatBlock/tests/schema.test.js +0 -166
  71. package/src/ChatBlock/tests/streamingService.test.js +0 -467
  72. package/src/ChatBlock/tests/useChatController.test.jsx +0 -268
  73. package/src/ChatBlock/tests/useChatStreaming.test.jsx +0 -163
  74. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +0 -107
  75. package/src/ChatBlock/tests/useMarked.test.jsx +0 -107
  76. package/src/ChatBlock/tests/useQualityMarkers.test.jsx +0 -150
  77. package/src/ChatBlock/tests/useScrollonStream.test.jsx +0 -121
  78. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +0 -151
  79. package/src/ChatBlock/tests/utils.test.jsx +0 -241
  80. package/src/ChatBlock/tests/withOnyxData.test.jsx +0 -81
  81. /package/src/ChatBlock/{schema.js → schema.jsx} +0 -0
@@ -1,166 +0,0 @@
1
- import '@testing-library/jest-dom';
2
- import { ChatBlockSchema } from '../schema';
3
-
4
- describe('ChatBlockSchema', () => {
5
- const mockAssistants = [
6
- { id: 1, name: 'Assistant 1' },
7
- { id: 2, name: 'Assistant 2' },
8
- ];
9
-
10
- it('returns schema object with title', () => {
11
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
12
- expect(schema.title).toBe('Chatbot');
13
- });
14
-
15
- it('returns schema with fieldsets', () => {
16
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
17
- expect(schema.fieldsets).toBeDefined();
18
- expect(Array.isArray(schema.fieldsets)).toBe(true);
19
- expect(schema.fieldsets.length).toBeGreaterThan(0);
20
- });
21
-
22
- it('returns schema with properties', () => {
23
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
24
- expect(schema.properties).toBeDefined();
25
- });
26
-
27
- it('includes assistant choices from assistants array', () => {
28
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
29
- expect(schema.properties.assistant.choices).toEqual([
30
- ['1', 'Assistant 1'],
31
- ['2', 'Assistant 2'],
32
- ]);
33
- });
34
-
35
- it('handles empty assistants array', () => {
36
- const schema = ChatBlockSchema({ assistants: [], data: {} });
37
- expect(schema.properties.assistant.choices).toEqual([]);
38
- });
39
-
40
- it('handles undefined assistants', () => {
41
- const schema = ChatBlockSchema({ assistants: undefined, data: {} });
42
- expect(schema.properties.assistant.choices).toEqual([]);
43
- });
44
-
45
- it('includes starterPrompts field when enableStarterPrompts is true', () => {
46
- const schema = ChatBlockSchema({
47
- assistants: mockAssistants,
48
- data: { enableStarterPrompts: true },
49
- });
50
- expect(schema.fieldsets[0].fields).toContain('starterPrompts');
51
- });
52
-
53
- it('excludes starterPrompts field when enableStarterPrompts is false', () => {
54
- const schema = ChatBlockSchema({
55
- assistants: mockAssistants,
56
- data: { enableStarterPrompts: false },
57
- });
58
- expect(schema.fieldsets[0].fields).not.toContain('starterPrompts');
59
- });
60
-
61
- it('includes feedbackReasons field when enableFeedback is true', () => {
62
- const schema = ChatBlockSchema({
63
- assistants: mockAssistants,
64
- data: { enableFeedback: true },
65
- });
66
- expect(schema.fieldsets[0].fields).toContain('feedbackReasons');
67
- });
68
-
69
- it('excludes feedbackReasons field when enableFeedback is false', () => {
70
- const schema = ChatBlockSchema({
71
- assistants: mockAssistants,
72
- data: { enableFeedback: false },
73
- });
74
- expect(schema.fieldsets[0].fields).not.toContain('feedbackReasons');
75
- });
76
-
77
- it('includes quality check fields when qualityCheck is enabled', () => {
78
- const schema = ChatBlockSchema({
79
- assistants: mockAssistants,
80
- data: { qualityCheck: 'enabled' },
81
- });
82
- expect(schema.fieldsets[0].fields).toContain('maxContextSegments');
83
- expect(schema.fieldsets[0].fields).toContain('noSupportDocumentsMessage');
84
- expect(schema.fieldsets[0].fields).toContain('qualityCheckContext');
85
- expect(schema.fieldsets[0].fields).toContain('qualityCheckStages');
86
- });
87
-
88
- it('excludes quality check fields when qualityCheck is disabled', () => {
89
- const schema = ChatBlockSchema({
90
- assistants: mockAssistants,
91
- data: { qualityCheck: 'disabled' },
92
- });
93
- expect(schema.fieldsets[0].fields).not.toContain('maxContextSegments');
94
- expect(schema.fieldsets[0].fields).not.toContain('qualityCheckContext');
95
- });
96
-
97
- it('includes onDemandInputToggle when qualityCheck is ondemand_toggle', () => {
98
- const schema = ChatBlockSchema({
99
- assistants: mockAssistants,
100
- data: { qualityCheck: 'ondemand_toggle' },
101
- });
102
- expect(schema.fieldsets[0].fields).toContain('onDemandInputToggle');
103
- });
104
-
105
- it('excludes onDemandInputToggle when qualityCheck is not ondemand_toggle', () => {
106
- const schema = ChatBlockSchema({
107
- assistants: mockAssistants,
108
- data: { qualityCheck: 'enabled' },
109
- });
110
- expect(schema.fieldsets[0].fields).not.toContain('onDemandInputToggle');
111
- });
112
-
113
- it('includes totalFailMessage when enableShowTotalFailMessage is true', () => {
114
- const schema = ChatBlockSchema({
115
- assistants: mockAssistants,
116
- data: { enableShowTotalFailMessage: true },
117
- });
118
- expect(schema.fieldsets[0].fields).toContain('totalFailMessage');
119
- });
120
-
121
- it('excludes totalFailMessage when enableShowTotalFailMessage is false', () => {
122
- const schema = ChatBlockSchema({
123
- assistants: mockAssistants,
124
- data: { enableShowTotalFailMessage: false },
125
- });
126
- expect(schema.fieldsets[0].fields).not.toContain('totalFailMessage');
127
- });
128
-
129
- it('has required array', () => {
130
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
131
- expect(schema.required).toBeDefined();
132
- expect(Array.isArray(schema.required)).toBe(true);
133
- });
134
-
135
- it('has default values for key properties', () => {
136
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
137
- expect(schema.properties.placeholderPrompt.default).toBe('Ask a question');
138
- expect(schema.properties.chatTitle.default).toBe('Online public chat');
139
- expect(schema.properties.enableFeedback.default).toBe(true);
140
- expect(schema.properties.qualityCheck.default).toBe('disabled');
141
- });
142
-
143
- it('has qualityCheckStages with default score ranges', () => {
144
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
145
- expect(schema.properties.qualityCheckStages.default).toBeDefined();
146
- expect(Array.isArray(schema.properties.qualityCheckStages.default)).toBe(
147
- true,
148
- );
149
- expect(schema.properties.qualityCheckStages.default.length).toBe(5);
150
- });
151
-
152
- it('has feedbackReasons with default choices', () => {
153
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
154
- expect(schema.properties.feedbackReasons.default).toBeDefined();
155
- expect(Array.isArray(schema.properties.feedbackReasons.default)).toBe(true);
156
- });
157
-
158
- it('has deepResearch property with choices', () => {
159
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
160
- expect(schema.properties.deepResearch.choices).toBeDefined();
161
- expect(schema.properties.deepResearch.choices).toContainEqual([
162
- 'always_on',
163
- 'Always on',
164
- ]);
165
- });
166
- });
@@ -1,467 +0,0 @@
1
- import { TextEncoder, TextDecoder } from 'util';
2
-
3
- import {
4
- processRawChunkString,
5
- handleStream,
6
- createChatSession,
7
- submitFeedback,
8
- regenerateMessage,
9
- } from '../services/streamingService';
10
- import { PacketType } from '../types/streamingModels';
11
-
12
- global.TextEncoder = TextEncoder;
13
- global.TextDecoder = TextDecoder;
14
-
15
- // ── processRawChunkString ──────────────────────────────────────────────
16
-
17
- describe('processRawChunkString', () => {
18
- it('returns empty array for empty string', () => {
19
- const [chunks, partial] = processRawChunkString('', null);
20
- expect(chunks).toEqual([]);
21
- expect(partial).toBeNull();
22
- });
23
-
24
- it('parses a single complete JSON chunk', () => {
25
- const raw = JSON.stringify({ ind: 1, obj: { type: 'message_delta' } });
26
- const [chunks, partial] = processRawChunkString(raw, null);
27
- expect(chunks).toHaveLength(1);
28
- expect(chunks[0]).toEqual({ ind: 1, obj: { type: 'message_delta' } });
29
- expect(partial).toBeNull();
30
- });
31
-
32
- it('parses multiple newline-separated JSON chunks', () => {
33
- const c1 = JSON.stringify({ ind: 1, obj: { type: 'message_start' } });
34
- const c2 = JSON.stringify({ ind: 2, obj: { type: 'message_delta' } });
35
- const raw = `${c1}\n${c2}`;
36
- const [chunks, partial] = processRawChunkString(raw, null);
37
- expect(chunks).toHaveLength(2);
38
- expect(chunks[0].ind).toBe(1);
39
- expect(chunks[1].ind).toBe(2);
40
- expect(partial).toBeNull();
41
- });
42
-
43
- it('handles incomplete chunk and returns it as partial', () => {
44
- const raw = '{"ind": 1, "obj": {"type"';
45
- const [chunks, partial] = processRawChunkString(raw, null);
46
- expect(chunks).toHaveLength(0);
47
- expect(partial).toBe(raw);
48
- });
49
-
50
- it('combines previous partial chunk with new data', () => {
51
- const prev = '{"ind": 1, "obj": {"type"';
52
- const rest = ': "message_delta"}}';
53
- const [chunks, partial] = processRawChunkString(rest, prev);
54
- expect(chunks).toHaveLength(1);
55
- expect(chunks[0]).toEqual({ ind: 1, obj: { type: 'message_delta' } });
56
- expect(partial).toBeNull();
57
- });
58
-
59
- it('filters out empty lines', () => {
60
- const c1 = JSON.stringify({ ind: 1, obj: { type: 'stop' } });
61
- const raw = `\n${c1}\n\n`;
62
- const [chunks, partial] = processRawChunkString(raw, null);
63
- expect(chunks).toHaveLength(1);
64
- expect(partial).toBeNull();
65
- });
66
-
67
- it('returns null for falsy input', () => {
68
- const [chunks, partial] = processRawChunkString(null, null);
69
- expect(chunks).toEqual([]);
70
- expect(partial).toBeNull();
71
- });
72
- });
73
-
74
- // ── handleStream ───────────────────────────────────────────────────────
75
-
76
- describe('handleStream', () => {
77
- function makeReadableStream(chunks) {
78
- let index = 0;
79
- return {
80
- body: {
81
- getReader() {
82
- return {
83
- async read() {
84
- if (index >= chunks.length) {
85
- return { done: true, value: undefined };
86
- }
87
- const value = new TextEncoder().encode(chunks[index++]);
88
- return { done: false, value };
89
- },
90
- };
91
- },
92
- },
93
- };
94
- }
95
-
96
- it('yields packets from a stream with ind/obj format', async () => {
97
- const packet = JSON.stringify({
98
- ind: 1,
99
- obj: { type: PacketType.MESSAGE_DELTA, content: 'hello' },
100
- });
101
- const response = makeReadableStream([packet]);
102
- const allPackets = [];
103
- for await (const packets of handleStream(response)) {
104
- allPackets.push(...packets);
105
- }
106
- expect(allPackets).toHaveLength(1);
107
- expect(allPackets[0].obj.type).toBe(PacketType.MESSAGE_DELTA);
108
- });
109
-
110
- it('handles MessageResponseIDInfo format', async () => {
111
- const chunk = JSON.stringify({
112
- user_message_id: 42,
113
- reserved_assistant_message_id: 43,
114
- });
115
- const response = makeReadableStream([chunk]);
116
- const allPackets = [];
117
- for await (const packets of handleStream(response)) {
118
- allPackets.push(...packets);
119
- }
120
- expect(allPackets).toHaveLength(1);
121
- expect(allPackets[0].obj.type).toBe(PacketType.MESSAGE_END_ID_INFO);
122
- expect(allPackets[0].obj.user_message_id).toBe(42);
123
- });
124
-
125
- it('handles error format', async () => {
126
- const chunk = JSON.stringify({ error: 'something went wrong' });
127
- const response = makeReadableStream([chunk]);
128
- const allPackets = [];
129
- for await (const packets of handleStream(response)) {
130
- allPackets.push(...packets);
131
- }
132
- expect(allPackets).toHaveLength(1);
133
- expect(allPackets[0].obj.type).toBe(PacketType.ERROR);
134
- expect(allPackets[0].obj.error).toBe('something went wrong');
135
- });
136
-
137
- it('throws when no reader is available', async () => {
138
- const response = { body: null };
139
- const gen = handleStream(response);
140
- await expect(gen.next()).rejects.toThrow('No reader available');
141
- });
142
-
143
- it('handles multiple chunks across reads', async () => {
144
- const c1 = JSON.stringify({
145
- ind: 1,
146
- obj: { type: PacketType.MESSAGE_START, content: 'hi' },
147
- });
148
- const c2 = JSON.stringify({
149
- ind: 2,
150
- obj: { type: PacketType.MESSAGE_DELTA, content: ' world' },
151
- });
152
- const response = makeReadableStream([c1, c2]);
153
- const allPackets = [];
154
- for await (const packets of handleStream(response)) {
155
- allPackets.push(...packets);
156
- }
157
- expect(allPackets).toHaveLength(2);
158
- });
159
-
160
- it('skips non-object chunks', async () => {
161
- // A string that parses to a non-object (number)
162
- const response = makeReadableStream(['42']);
163
- const allPackets = [];
164
- for await (const packets of handleStream(response)) {
165
- allPackets.push(...packets);
166
- }
167
- expect(allPackets).toHaveLength(0);
168
- });
169
-
170
- it('handles split chunk across two reads', async () => {
171
- const full = JSON.stringify({
172
- ind: 1,
173
- obj: { type: PacketType.STOP },
174
- });
175
- const half1 = full.slice(0, 10);
176
- const half2 = full.slice(10);
177
- const response = makeReadableStream([half1, half2]);
178
- const allPackets = [];
179
- for await (const packets of handleStream(response)) {
180
- allPackets.push(...packets);
181
- }
182
- expect(allPackets).toHaveLength(1);
183
- expect(allPackets[0].obj.type).toBe(PacketType.STOP);
184
- });
185
- });
186
-
187
- // ── createChatSession ──────────────────────────────────────────────────
188
-
189
- describe('createChatSession', () => {
190
- beforeEach(() => {
191
- global.fetch = jest.fn();
192
- });
193
-
194
- afterEach(() => {
195
- jest.restoreAllMocks();
196
- });
197
-
198
- it('returns chat session id on success', async () => {
199
- global.fetch.mockResolvedValue({
200
- ok: true,
201
- json: () => Promise.resolve({ chat_session_id: 'abc-123' }),
202
- });
203
-
204
- const result = await createChatSession(1, 'Test session');
205
- expect(result).toBe('abc-123');
206
- expect(global.fetch).toHaveBeenCalledWith(
207
- '/_da/chat/create-chat-session',
208
- expect.objectContaining({
209
- method: 'POST',
210
- body: JSON.stringify({ persona_id: 1, description: 'Test session' }),
211
- }),
212
- );
213
- });
214
-
215
- it('throws on failed response', async () => {
216
- global.fetch.mockResolvedValue({ ok: false });
217
- await expect(createChatSession(1)).rejects.toThrow(
218
- 'Failed to create chat session',
219
- );
220
- });
221
- });
222
-
223
- // ── submitFeedback ─────────────────────────────────────────────────────
224
-
225
- describe('submitFeedback', () => {
226
- beforeEach(() => {
227
- global.fetch = jest.fn();
228
- });
229
-
230
- afterEach(() => {
231
- jest.restoreAllMocks();
232
- });
233
-
234
- it('sends positive feedback', async () => {
235
- global.fetch.mockResolvedValue({
236
- ok: true,
237
- json: () => Promise.resolve({}),
238
- });
239
-
240
- await submitFeedback({
241
- chatMessageId: 1,
242
- isPositive: true,
243
- feedbackText: 'Great!',
244
- });
245
-
246
- const body = JSON.parse(global.fetch.mock.calls[0][1].body);
247
- expect(body.is_positive).toBe(true);
248
- expect(body.chat_message_id).toBe(1);
249
- expect(body.predefined_feedback).toBeUndefined();
250
- });
251
-
252
- it('sends negative feedback with predefined reason', async () => {
253
- global.fetch.mockResolvedValue({
254
- ok: true,
255
- json: () => Promise.resolve({}),
256
- });
257
-
258
- await submitFeedback({
259
- chatMessageId: 2,
260
- isPositive: false,
261
- predefinedFeedback: 'Inaccurate',
262
- });
263
-
264
- const body = JSON.parse(global.fetch.mock.calls[0][1].body);
265
- expect(body.is_positive).toBe(false);
266
- expect(body.predefined_feedback).toBe('Inaccurate');
267
- });
268
-
269
- it('throws on failed response', async () => {
270
- global.fetch.mockResolvedValue({ ok: false });
271
- await expect(
272
- submitFeedback({ chatMessageId: 1, isPositive: true }),
273
- ).rejects.toThrow('Failed to submit feedback');
274
- });
275
- });
276
-
277
- // ── sendMessage ──────────────────────────────────────────────────────
278
-
279
- describe('sendMessage', () => {
280
- const { sendMessage } = require('../services/streamingService');
281
-
282
- beforeEach(() => {
283
- global.fetch = jest.fn();
284
- });
285
-
286
- afterEach(() => {
287
- jest.restoreAllMocks();
288
- });
289
-
290
- function makeStreamResponse(chunks) {
291
- let index = 0;
292
- return {
293
- ok: true,
294
- body: {
295
- getReader() {
296
- return {
297
- async read() {
298
- if (index >= chunks.length) {
299
- return { done: true, value: undefined };
300
- }
301
- const value = new TextEncoder().encode(chunks[index++]);
302
- return { done: false, value };
303
- },
304
- };
305
- },
306
- },
307
- };
308
- }
309
-
310
- it('sends message and yields packets', async () => {
311
- const chunk = JSON.stringify({
312
- ind: 1,
313
- obj: { type: 'message_delta', content: 'hello' },
314
- });
315
- global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
316
-
317
- const allPackets = [];
318
- for await (const packets of sendMessage({
319
- message: 'test',
320
- chatSessionId: 'session-1',
321
- parentMessageId: null,
322
- regenerate: false,
323
- filters: null,
324
- selectedDocumentIds: [],
325
- })) {
326
- allPackets.push(...packets);
327
- }
328
-
329
- expect(allPackets).toHaveLength(1);
330
- expect(allPackets[0].obj.content).toBe('hello');
331
- expect(global.fetch).toHaveBeenCalledWith(
332
- '/_da/chat/send-message',
333
- expect.objectContaining({ method: 'POST' }),
334
- );
335
- });
336
-
337
- it('uses _rq middleware for related questions', async () => {
338
- const chunk = JSON.stringify({
339
- ind: 1,
340
- obj: { type: 'stop' },
341
- });
342
- global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
343
-
344
- const allPackets = [];
345
- for await (const packets of sendMessage(
346
- {
347
- message: 'test',
348
- chatSessionId: 'session-1',
349
- parentMessageId: null,
350
- regenerate: false,
351
- filters: null,
352
- selectedDocumentIds: [],
353
- },
354
- true,
355
- )) {
356
- allPackets.push(...packets);
357
- }
358
-
359
- expect(global.fetch).toHaveBeenCalledWith(
360
- '/_rq/chat/send-message',
361
- expect.objectContaining({ method: 'POST' }),
362
- );
363
- });
364
-
365
- it('throws on failed response', async () => {
366
- global.fetch.mockResolvedValue({
367
- ok: false,
368
- json: () => Promise.resolve({ message: 'Bad request' }),
369
- });
370
-
371
- const gen = sendMessage({
372
- message: 'test',
373
- chatSessionId: 'session-1',
374
- parentMessageId: null,
375
- regenerate: false,
376
- filters: null,
377
- selectedDocumentIds: [],
378
- });
379
-
380
- await expect(gen.next()).rejects.toThrow('Failed to send message');
381
- });
382
-
383
- it('includes selectedDocumentIds in payload when provided', async () => {
384
- const chunk = JSON.stringify({
385
- ind: 1,
386
- obj: { type: 'stop' },
387
- });
388
- global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
389
-
390
- // eslint-disable-next-line no-unused-vars
391
- for await (const _ of sendMessage({
392
- message: 'test',
393
- chatSessionId: 'session-1',
394
- parentMessageId: null,
395
- regenerate: false,
396
- filters: null,
397
- selectedDocumentIds: ['doc1', 'doc2'],
398
- })) {
399
- // consume
400
- }
401
-
402
- const body = JSON.parse(global.fetch.mock.calls[0][1].body);
403
- expect(body.search_doc_ids).toEqual(['doc1', 'doc2']);
404
- });
405
-
406
- it('includes LLM override when temperature is set', async () => {
407
- const chunk = JSON.stringify({
408
- ind: 1,
409
- obj: { type: 'stop' },
410
- });
411
- global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
412
-
413
- // eslint-disable-next-line no-unused-vars
414
- for await (const _ of sendMessage({
415
- message: 'test',
416
- chatSessionId: 'session-1',
417
- parentMessageId: null,
418
- regenerate: false,
419
- filters: null,
420
- selectedDocumentIds: [],
421
- temperature: 0.7,
422
- modelVersion: 'gpt-4',
423
- })) {
424
- // consume
425
- }
426
-
427
- const body = JSON.parse(global.fetch.mock.calls[0][1].body);
428
- expect(body.llm_override).toEqual(
429
- expect.objectContaining({
430
- temperature: 0.7,
431
- model_version: 'gpt-4',
432
- }),
433
- );
434
- });
435
- });
436
-
437
- // ── regenerateMessage ──────────────────────────────────────────────────
438
-
439
- describe('regenerateMessage', () => {
440
- beforeEach(() => {
441
- global.fetch = jest.fn();
442
- });
443
-
444
- afterEach(() => {
445
- jest.restoreAllMocks();
446
- });
447
-
448
- it('sends regenerate request', async () => {
449
- global.fetch.mockResolvedValue({ ok: true });
450
- await regenerateMessage(1, 2);
451
-
452
- expect(global.fetch).toHaveBeenCalledWith(
453
- '/_da/chat/regenerate-message',
454
- expect.objectContaining({
455
- method: 'POST',
456
- body: JSON.stringify({ message_id: 1, chat_session_id: 2 }),
457
- }),
458
- );
459
- });
460
-
461
- it('throws on failed response', async () => {
462
- global.fetch.mockResolvedValue({ ok: false });
463
- await expect(regenerateMessage(1, 2)).rejects.toThrow(
464
- 'Failed to regenerate message',
465
- );
466
- });
467
- });