@eeacms/volto-eea-chatbot 2.0.1 → 2.0.3

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 (84) hide show
  1. package/.eslintrc.js +6 -6
  2. package/CHANGELOG.md +20 -0
  3. package/artifacts/ONYX_V3_INTEGRATION.md +34 -0
  4. package/jest-addon.config.js +2 -1
  5. package/package.json +1 -1
  6. package/src/ChatBlock/ChatBlockEdit.jsx +2 -1
  7. package/src/ChatBlock/chat/AIMessage.tsx +36 -16
  8. package/src/ChatBlock/chat/ChatMessage.tsx +1 -1
  9. package/src/ChatBlock/chat/ChatWindow.tsx +13 -11
  10. package/src/ChatBlock/chat/UserMessage.tsx +4 -4
  11. package/src/ChatBlock/components/AutoResizeTextarea.jsx +1 -1
  12. package/src/ChatBlock/components/ChatMessageFeedback.jsx +2 -2
  13. package/src/ChatBlock/components/EmptyState.jsx +1 -1
  14. package/src/ChatBlock/components/FeedbackModal.jsx +1 -1
  15. package/src/ChatBlock/components/HalloumiFeedback.jsx +2 -2
  16. package/src/ChatBlock/components/Source.jsx +2 -2
  17. package/src/ChatBlock/components/UserActionsToolbar.jsx +3 -3
  18. package/src/ChatBlock/components/WebResultIcon.tsx +2 -2
  19. package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
  20. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +4 -4
  21. package/src/ChatBlock/components/markdown/{index.js → index.jsx} +1 -1
  22. package/src/ChatBlock/hooks/useChatController.ts +67 -14
  23. package/src/ChatBlock/hooks/useChatStreaming.ts +4 -4
  24. package/src/ChatBlock/hooks/useToolDisplayTiming.ts +2 -1
  25. package/src/ChatBlock/packets/MultiToolRenderer.tsx +86 -56
  26. package/src/ChatBlock/packets/RendererComponent.tsx +13 -5
  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 +14 -9
  31. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +6 -5
  32. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +30 -21
  33. package/src/ChatBlock/{schema.js → schema.jsx} +13 -0
  34. package/src/ChatBlock/services/messageProcessor.ts +72 -17
  35. package/src/ChatBlock/services/packetUtils.ts +13 -3
  36. package/src/ChatBlock/services/streamingService.ts +155 -68
  37. package/src/ChatBlock/types/streamingModels.ts +47 -2
  38. package/src/ChatBlock/utils/citations.ts +1 -1
  39. package/src/halloumi/filtering.test.js +199 -1
  40. package/src/middleware.js +18 -1
  41. package/src/middleware.test.js +14 -0
  42. package/src/ChatBlock/tests/AIMessage.test.jsx +0 -95
  43. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +0 -49
  44. package/src/ChatBlock/tests/BlinkingDot.test.jsx +0 -71
  45. package/src/ChatBlock/tests/ChatMessage.test.jsx +0 -75
  46. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +0 -73
  47. package/src/ChatBlock/tests/Citation.test.jsx +0 -107
  48. package/src/ChatBlock/tests/ClaimModal.test.jsx +0 -136
  49. package/src/ChatBlock/tests/ClaimSegments.test.jsx +0 -206
  50. package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +0 -241
  51. package/src/ChatBlock/tests/EmptyState.test.jsx +0 -137
  52. package/src/ChatBlock/tests/FeedbackModal.test.jsx +0 -138
  53. package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +0 -161
  54. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +0 -94
  55. package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +0 -178
  56. package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +0 -227
  57. package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +0 -134
  58. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +0 -105
  59. package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +0 -163
  60. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +0 -215
  61. package/src/ChatBlock/tests/RenderClaimView.test.jsx +0 -191
  62. package/src/ChatBlock/tests/RendererComponent.test.jsx +0 -139
  63. package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +0 -295
  64. package/src/ChatBlock/tests/Source.test.jsx +0 -79
  65. package/src/ChatBlock/tests/SourceChip.test.jsx +0 -108
  66. package/src/ChatBlock/tests/Spinner.test.jsx +0 -18
  67. package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +0 -135
  68. package/src/ChatBlock/tests/UserMessage.test.jsx +0 -83
  69. package/src/ChatBlock/tests/WebResultIcon.test.jsx +0 -61
  70. package/src/ChatBlock/tests/citations.test.js +0 -114
  71. package/src/ChatBlock/tests/index.test.js +0 -51
  72. package/src/ChatBlock/tests/messageProcessor.test.jsx +0 -438
  73. package/src/ChatBlock/tests/packetUtils.test.js +0 -158
  74. package/src/ChatBlock/tests/schema.test.js +0 -166
  75. package/src/ChatBlock/tests/streamingService.test.js +0 -467
  76. package/src/ChatBlock/tests/useChatController.test.jsx +0 -268
  77. package/src/ChatBlock/tests/useChatStreaming.test.jsx +0 -163
  78. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +0 -107
  79. package/src/ChatBlock/tests/useMarked.test.jsx +0 -107
  80. package/src/ChatBlock/tests/useQualityMarkers.test.jsx +0 -150
  81. package/src/ChatBlock/tests/useScrollonStream.test.jsx +0 -121
  82. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +0 -151
  83. package/src/ChatBlock/tests/utils.test.jsx +0 -241
  84. package/src/ChatBlock/tests/withOnyxData.test.jsx +0 -81
@@ -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
- });