@aleph-alpha/chat-kit 1.3.1 → 1.4.0

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 (56) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-CyGLItQZ.js → CkInput.vue_vue_type_script_setup_true_lang-C5C09u0z.js} +9 -8
  2. package/dist/adapters/index.d.ts +18 -0
  3. package/dist/adapters/index.d.ts.map +1 -0
  4. package/dist/adapters/index.js +4 -0
  5. package/dist/adapters/responses-api.d.ts +180 -0
  6. package/dist/adapters/responses-api.d.ts.map +1 -0
  7. package/dist/adapters/responses-api.js +153 -0
  8. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts +1 -1
  9. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +11 -3
  10. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
  11. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +5 -1
  12. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
  13. package/dist/components/index.js +1 -1
  14. package/dist/composables/index.js +1 -1
  15. package/dist/composables/useConversations.d.ts +12 -0
  16. package/dist/composables/useConversations.d.ts.map +1 -1
  17. package/dist/generateId-BHf0W4Rq.js +6 -0
  18. package/dist/handlers/index.js +1 -1
  19. package/dist/handlers/useMessageHandler.d.ts +2 -2
  20. package/dist/handlers/useMessageHandler.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -8
  24. package/dist/services/index.d.ts +0 -2
  25. package/dist/services/index.d.ts.map +1 -1
  26. package/dist/services/index.js +1 -37
  27. package/dist/services/types.d.ts +2 -2
  28. package/dist/services/types.d.ts.map +1 -1
  29. package/dist/types.d.ts +47 -2
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/{useConversationSessions-ulFhtpQr.js → useConversationSessions-GLZV_Ctr.js} +18 -2
  32. package/dist/{useMessageHandler-BmBdu2EW.js → useMessageHandler-ZRC-gIOJ.js} +5 -5
  33. package/package.json +10 -2
  34. package/src/adapters/index.ts +17 -0
  35. package/src/adapters/responses-api.spec.ts +365 -0
  36. package/src/adapters/responses-api.ts +444 -0
  37. package/src/components/composed/CkConversation/CkConversation.spec.ts +171 -25
  38. package/src/components/composed/CkConversation/CkConversation.stories.ts +266 -46
  39. package/src/components/composed/CkConversation/CkConversation.vue +19 -7
  40. package/src/composables/useConversations.spec.ts +118 -0
  41. package/src/composables/useConversations.ts +32 -0
  42. package/src/handlers/useChatKitLabels.spec.ts +16 -2
  43. package/src/handlers/useMessageHandler.spec.ts +112 -44
  44. package/src/handlers/useMessageHandler.ts +16 -2
  45. package/src/helpers/conversationTree.spec.ts +46 -19
  46. package/src/index.ts +11 -2
  47. package/src/services/index.ts +0 -2
  48. package/src/services/types.ts +2 -2
  49. package/src/types.ts +55 -2
  50. package/dist/ChatService-LogwJNxh.js +0 -9
  51. package/dist/services/ChatService.d.ts +0 -42
  52. package/dist/services/ChatService.d.ts.map +0 -1
  53. package/dist/services/MockChatService.d.ts +0 -10
  54. package/dist/services/MockChatService.d.ts.map +0 -1
  55. package/src/services/ChatService.ts +0 -60
  56. package/src/services/MockChatService.ts +0 -47
@@ -1,3 +1,4 @@
1
+ import type { ResponseList, StoredResponse } from '../adapters/responses-api';
1
2
  import type {
2
3
  HttpClient,
3
4
  RawConversation,
@@ -554,4 +555,121 @@ describe('useConversations', () => {
554
555
  expect(conv.getById('missing')).toBeUndefined();
555
556
  });
556
557
  });
558
+
559
+ describe('fetchResponses', () => {
560
+ function makeStoredResponse(
561
+ overrides: Partial<StoredResponse> & { id: string },
562
+ ): StoredResponse {
563
+ return {
564
+ object: 'response',
565
+ created_at: 1_777_456_700,
566
+ completed_at: 1_777_456_701,
567
+ status: 'completed',
568
+ model: 'pharia-1',
569
+ output: [],
570
+ request_input: [],
571
+ ...overrides,
572
+ };
573
+ }
574
+
575
+ function makeResponseList(data: StoredResponse[]): ResponseList {
576
+ return { object: 'list', data };
577
+ }
578
+
579
+ it('GETs /:id/responses with order=asc by default and returns the unwrapped list', async () => {
580
+ const http = makeFakeHttp();
581
+ http.get.mockResolvedValue(
582
+ makeResponseList([
583
+ makeStoredResponse({ id: 'resp_1' }),
584
+ makeStoredResponse({ id: 'resp_2' }),
585
+ ]),
586
+ );
587
+
588
+ const conv = useConversations({
589
+ httpClient: asHttp(http),
590
+ path: '/conversations',
591
+ });
592
+
593
+ const result = await conv.fetchResponses('conv_22');
594
+
595
+ expect(http.get).toHaveBeenCalledWith(
596
+ '/conversations/conv_22/responses',
597
+ {
598
+ params: { order: 'asc' },
599
+ },
600
+ );
601
+ expect(result.map((r) => r.id)).toEqual(['resp_1', 'resp_2']);
602
+ });
603
+
604
+ it('passes through the caller-supplied order', async () => {
605
+ const http = makeFakeHttp();
606
+ http.get.mockResolvedValue(makeResponseList([]));
607
+
608
+ const conv = useConversations({
609
+ httpClient: asHttp(http),
610
+ path: '/conversations',
611
+ });
612
+
613
+ await conv.fetchResponses('conv_22', { order: 'desc' });
614
+
615
+ expect(http.get).toHaveBeenCalledWith(
616
+ '/conversations/conv_22/responses',
617
+ {
618
+ params: { order: 'desc' },
619
+ },
620
+ );
621
+ });
622
+
623
+ it('does not modify the conversations list', async () => {
624
+ const http = makeFakeHttp();
625
+ http.get
626
+ .mockResolvedValueOnce(makeRawList([makeRawConversation({ id: 'a' })]))
627
+ .mockResolvedValueOnce(
628
+ makeResponseList([makeStoredResponse({ id: 'resp_1' })]),
629
+ );
630
+
631
+ const conv = useConversations({
632
+ httpClient: asHttp(http),
633
+ path: '/conversations',
634
+ });
635
+ await conv.fetch();
636
+ await conv.fetchResponses('a');
637
+
638
+ expect(conv.conversations.value.map((c) => c.id)).toEqual(['a']);
639
+ });
640
+
641
+ it('toggles isLoading around the call', async () => {
642
+ const http = makeFakeHttp();
643
+ let resolve!: (value: ResponseList) => void;
644
+ http.get.mockReturnValue(new Promise((r) => (resolve = r)));
645
+
646
+ const conv = useConversations({
647
+ httpClient: asHttp(http),
648
+ path: '/conversations',
649
+ });
650
+ const promise = conv.fetchResponses('a');
651
+
652
+ await nextTick();
653
+ expect(conv.isLoading.value).toBe(true);
654
+
655
+ resolve(makeResponseList([]));
656
+ await promise;
657
+
658
+ expect(conv.isLoading.value).toBe(false);
659
+ });
660
+
661
+ it('records and re-throws errors', async () => {
662
+ const http = makeFakeHttp();
663
+ http.get.mockRejectedValue(new Error('boom'));
664
+
665
+ const conv = useConversations({
666
+ httpClient: asHttp(http),
667
+ path: '/conversations',
668
+ });
669
+
670
+ await expect(conv.fetchResponses('a')).rejects.toThrow('boom');
671
+ expect(conv.error.value?.message).toBe('boom');
672
+ expect(conv.isLoading.value).toBe(false);
673
+ });
674
+ });
557
675
  });
@@ -1,3 +1,4 @@
1
+ import type { ResponseList, StoredResponse } from '../adapters/responses-api';
1
2
  import { generateId, secondsToDate, toError } from '../helpers';
2
3
  import type {
3
4
  ConversationSummary,
@@ -85,6 +86,18 @@ export interface UseConversationsReturn {
85
86
  remove(id: string, options?: MutationOptions): Promise<void>;
86
87
  /** Local lookup. Does not fetch — only checks the in-memory list. */
87
88
  getById(id: string): ConversationSummary | undefined;
89
+ /**
90
+ * List the OpenAI Responses-API records attached to a conversation. Pure
91
+ * fetch: it does not touch the conversations list. The endpoint is not
92
+ * paginated, so the returned array is the full timeline ordered by
93
+ * `created_at` (ascending by default). Pair with `responsesToTree` from
94
+ * `@aleph-alpha/chat-kit/adapters/responses-api` to hydrate a chat
95
+ * handler: `setTree(responsesToTree(await fetchResponses(id)))`.
96
+ */
97
+ fetchResponses(
98
+ id: string,
99
+ options?: { order?: 'asc' | 'desc' },
100
+ ): Promise<StoredResponse[]>;
88
101
  }
89
102
 
90
103
  const DEFAULT_PAGE_SIZE = 20;
@@ -277,6 +290,24 @@ export function useConversations(
277
290
  return conversations.value.find((c) => c.id === id);
278
291
  }
279
292
 
293
+ async function fetchResponses(
294
+ id: string,
295
+ opts?: { order?: 'asc' | 'desc' },
296
+ ): Promise<StoredResponse[]> {
297
+ beginRequest();
298
+ try {
299
+ const raw = await http.get<ResponseList>(`${path}/${id}/responses`, {
300
+ params: { order: opts?.order ?? 'asc' },
301
+ });
302
+ return raw.data;
303
+ } catch (e) {
304
+ error.value = toError(e);
305
+ throw error.value;
306
+ } finally {
307
+ endRequest();
308
+ }
309
+ }
310
+
280
311
  return {
281
312
  conversations: readonly(conversations) as Readonly<
282
313
  Ref<readonly ConversationSummary[]>
@@ -291,6 +322,7 @@ export function useConversations(
291
322
  rename,
292
323
  remove,
293
324
  getById,
325
+ fetchResponses,
294
326
  };
295
327
  }
296
328
 
@@ -261,7 +261,14 @@ describe('CkConversation uses injected labels', () => {
261
261
  {
262
262
  id: '1',
263
263
  role: 'assistant',
264
- content: 'Hi',
264
+ content: [
265
+ {
266
+ type: 'text',
267
+ id: 'txt-1',
268
+ text: 'Hi',
269
+ status: 'completed',
270
+ },
271
+ ],
265
272
  createdAt: new Date(),
266
273
  status: 'completed',
267
274
  },
@@ -290,7 +297,14 @@ describe('CkConversation uses injected labels', () => {
290
297
  {
291
298
  id: '1',
292
299
  role: 'assistant',
293
- content: 'Hi',
300
+ content: [
301
+ {
302
+ type: 'text',
303
+ id: 'txt-1',
304
+ text: 'Hi',
305
+ status: 'completed',
306
+ },
307
+ ],
294
308
  createdAt: new Date(),
295
309
  status: 'completed',
296
310
  },
@@ -1,6 +1,29 @@
1
- import type { ConversationTree, TreeMessage } from '../types';
1
+ import type {
2
+ ConversationTree,
3
+ MessageContent,
4
+ MessageTextContent,
5
+ TreeMessage,
6
+ } from '../types';
2
7
  import { useMessageHandler } from './useMessageHandler';
3
8
 
9
+ let textIdCounter = 0;
10
+ function text(value: string): MessageTextContent {
11
+ textIdCounter += 1;
12
+ return {
13
+ type: 'text',
14
+ id: `txt-${textIdCounter}`,
15
+ text: value,
16
+ status: 'completed',
17
+ };
18
+ }
19
+
20
+ function getText(content: MessageContent[]): string {
21
+ return content
22
+ .filter((c): c is MessageTextContent => c.type === 'text')
23
+ .map((c) => c.text)
24
+ .join('');
25
+ }
26
+
4
27
  function makeTreeMessage(
5
28
  overrides: Partial<TreeMessage> & { id: string },
6
29
  ): TreeMessage {
@@ -9,7 +32,7 @@ function makeTreeMessage(
9
32
  childrenIds: [],
10
33
  activeChildIndex: 0,
11
34
  role: 'user',
12
- content: '',
35
+ content: [],
13
36
  status: 'completed',
14
37
  createdAt: new Date('2025-01-01'),
15
38
  ...overrides,
@@ -43,11 +66,26 @@ describe('useMessageHandler', () => {
43
66
  const msg = handler.commitUserMessage();
44
67
 
45
68
  expect(msg.role).toBe('user');
46
- expect(msg.content).toBe('Hello');
69
+ expect(getText(msg.content)).toBe('Hello');
47
70
  expect(handler.messages.value).toHaveLength(1);
48
71
  expect(handler.messages.value[0].id).toBe(msg.id);
49
72
  });
50
73
 
74
+ it('produces a single text content item with status completed', () => {
75
+ const handler = useMessageHandler();
76
+ handler.setCurrentUserMessage('Hello');
77
+ const msg = handler.commitUserMessage();
78
+
79
+ expect(msg.content).toHaveLength(1);
80
+ const part = msg.content[0];
81
+ expect(part.type).toBe('text');
82
+ if (part.type === 'text') {
83
+ expect(part.text).toBe('Hello');
84
+ expect(part.status).toBe('completed');
85
+ expect(part.id).toBeTruthy();
86
+ }
87
+ });
88
+
51
89
  it('appends consecutive messages in order', () => {
52
90
  const handler = useMessageHandler();
53
91
 
@@ -90,25 +128,25 @@ describe('useMessageHandler', () => {
90
128
 
91
129
  handler.updateMessage({
92
130
  id: msg.id,
93
- content: 'Updated',
131
+ content: [text('Updated')],
94
132
  role: 'user',
95
133
  status: 'completed',
96
134
  });
97
135
 
98
- expect(handler.messages.value[0].content).toBe('Updated');
136
+ expect(getText(handler.messages.value[0].content)).toBe('Updated');
99
137
  });
100
138
 
101
139
  it('creates a new node when id does not exist (upsert)', () => {
102
140
  const handler = useMessageHandler();
103
141
  handler.updateMessage({
104
142
  id: 'new-1',
105
- content: 'Upserted',
143
+ content: [text('Upserted')],
106
144
  role: 'assistant',
107
145
  status: 'completed',
108
146
  });
109
147
 
110
148
  expect(handler.messages.value).toHaveLength(1);
111
- expect(handler.messages.value[0].content).toBe('Upserted');
149
+ expect(getText(handler.messages.value[0].content)).toBe('Upserted');
112
150
  expect(handler.messages.value[0].id).toBe('new-1');
113
151
  });
114
152
 
@@ -119,14 +157,40 @@ describe('useMessageHandler', () => {
119
157
 
120
158
  handler.updateMessage({
121
159
  id: 'assistant-1',
122
- content: 'Reply',
160
+ content: [text('Reply')],
123
161
  role: 'assistant',
124
162
  status: 'completed',
125
163
  });
126
164
 
127
165
  expect(handler.messages.value).toHaveLength(2);
128
166
  expect(handler.messages.value[1].id).toBe('assistant-1');
129
- expect(handler.messages.value[1].content).toBe('Reply');
167
+ expect(getText(handler.messages.value[1].content)).toBe('Reply');
168
+ });
169
+
170
+ it('preserves multi-part content (text + tool + reasoning)', () => {
171
+ const handler = useMessageHandler();
172
+ const content: MessageContent[] = [
173
+ { type: 'reasoning', id: 'r1', status: 'completed' },
174
+ { type: 'tool', id: 't1', name: 'search' },
175
+ {
176
+ type: 'text',
177
+ id: 'txt-multi',
178
+ text: 'Final answer',
179
+ status: 'completed',
180
+ },
181
+ ];
182
+ handler.updateMessage({
183
+ id: 'a-multi',
184
+ content,
185
+ role: 'assistant',
186
+ status: 'completed',
187
+ });
188
+
189
+ const stored = handler.messages.value[0];
190
+ expect(stored.content).toHaveLength(3);
191
+ expect(stored.content[0].type).toBe('reasoning');
192
+ expect(stored.content[1].type).toBe('tool');
193
+ expect(stored.content[2].type).toBe('text');
130
194
  });
131
195
 
132
196
  it('updates a mid-tree node in place without truncating the path', () => {
@@ -134,23 +198,23 @@ describe('useMessageHandler', () => {
134
198
  handler.setTree(
135
199
  makeTree(
136
200
  {
137
- u1: makeTreeMessage({ id: 'u1', content: 'First' }),
201
+ u1: makeTreeMessage({ id: 'u1', content: [text('First')] }),
138
202
  a1: makeTreeMessage({
139
203
  id: 'a1',
140
204
  parentId: 'u1',
141
205
  role: 'assistant',
142
- content: 'Reply 1',
206
+ content: [text('Reply 1')],
143
207
  }),
144
208
  u2: makeTreeMessage({
145
209
  id: 'u2',
146
210
  parentId: 'a1',
147
- content: 'Second',
211
+ content: [text('Second')],
148
212
  }),
149
213
  a2: makeTreeMessage({
150
214
  id: 'a2',
151
215
  parentId: 'u2',
152
216
  role: 'assistant',
153
- content: 'Reply 2',
217
+ content: [text('Reply 2')],
154
218
  }),
155
219
  },
156
220
  'u1',
@@ -159,13 +223,13 @@ describe('useMessageHandler', () => {
159
223
 
160
224
  handler.updateMessage({
161
225
  id: 'a1',
162
- content: 'Regenerated',
226
+ content: [text('Regenerated')],
163
227
  role: 'assistant',
164
228
  status: 'sending',
165
229
  });
166
230
 
167
231
  expect(handler.messages.value).toHaveLength(4);
168
- expect(handler.messages.value[1].content).toBe('Regenerated');
232
+ expect(getText(handler.messages.value[1].content)).toBe('Regenerated');
169
233
  });
170
234
 
171
235
  it('does not truncate when updating the last message', () => {
@@ -173,12 +237,12 @@ describe('useMessageHandler', () => {
173
237
  handler.setTree(
174
238
  makeTree(
175
239
  {
176
- u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
240
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
177
241
  a1: makeTreeMessage({
178
242
  id: 'a1',
179
243
  parentId: 'u1',
180
244
  role: 'assistant',
181
- content: 'Hi',
245
+ content: [text('Hi')],
182
246
  }),
183
247
  },
184
248
  'u1',
@@ -187,13 +251,13 @@ describe('useMessageHandler', () => {
187
251
 
188
252
  handler.updateMessage({
189
253
  id: 'a1',
190
- content: 'Streaming...',
254
+ content: [text('Streaming...')],
191
255
  role: 'assistant',
192
256
  status: 'sending',
193
257
  });
194
258
 
195
259
  expect(handler.messages.value).toHaveLength(2);
196
- expect(handler.messages.value[1].content).toBe('Streaming...');
260
+ expect(getText(handler.messages.value[1].content)).toBe('Streaming...');
197
261
  });
198
262
  });
199
263
 
@@ -206,7 +270,7 @@ describe('useMessageHandler', () => {
206
270
  const found = handler.getMessageById(msg.id);
207
271
  expect(found).toBeDefined();
208
272
  expect(found?.id).toBe(msg.id);
209
- expect(found?.content).toBe('Hello');
273
+ expect(getText(found?.content ?? [])).toBe('Hello');
210
274
  expect(found?.role).toBe('user');
211
275
  });
212
276
 
@@ -240,7 +304,7 @@ describe('useMessageHandler', () => {
240
304
 
241
305
  const renamed = handler.getMessageById('renamed');
242
306
  expect(renamed?.role).toBe(role);
243
- expect(renamed?.content).toBe(content);
307
+ expect(renamed?.content).toEqual(content);
244
308
  expect(renamed?.status).toBe(status);
245
309
  expect(renamed?.createdAt).toEqual(createdAt);
246
310
  });
@@ -250,14 +314,18 @@ describe('useMessageHandler', () => {
250
314
  handler.setTree(
251
315
  makeTree(
252
316
  {
253
- a: makeTreeMessage({ id: 'a', content: 'A' }),
317
+ a: makeTreeMessage({ id: 'a', content: [text('A')] }),
254
318
  b: makeTreeMessage({
255
319
  id: 'b',
256
320
  parentId: 'a',
257
321
  role: 'assistant',
258
- content: 'B',
322
+ content: [text('B')],
323
+ }),
324
+ c: makeTreeMessage({
325
+ id: 'c',
326
+ parentId: 'b',
327
+ content: [text('C')],
259
328
  }),
260
- c: makeTreeMessage({ id: 'c', parentId: 'b', content: 'C' }),
261
329
  },
262
330
  'a',
263
331
  ),
@@ -291,7 +359,7 @@ describe('useMessageHandler', () => {
291
359
  handler.commitUserMessage();
292
360
  handler.updateMessage({
293
361
  id: 'a1',
294
- content: 'Reply',
362
+ content: [text('Reply')],
295
363
  role: 'assistant',
296
364
  status: 'completed',
297
365
  });
@@ -323,7 +391,7 @@ describe('useMessageHandler', () => {
323
391
 
324
392
  handler.updateMessage({
325
393
  id: 'a1',
326
- content: 'Reply 1',
394
+ content: [text('Reply 1')],
327
395
  role: 'assistant',
328
396
  status: 'completed',
329
397
  });
@@ -333,7 +401,7 @@ describe('useMessageHandler', () => {
333
401
 
334
402
  handler.updateMessage({
335
403
  id: 'a2',
336
- content: 'Reply 2',
404
+ content: [text('Reply 2')],
337
405
  role: 'assistant',
338
406
  status: 'completed',
339
407
  });
@@ -341,7 +409,7 @@ describe('useMessageHandler', () => {
341
409
  const roles = handler.messages.value.map((m) => m.role);
342
410
  expect(roles).toEqual(['user', 'assistant', 'user', 'assistant']);
343
411
 
344
- const contents = handler.messages.value.map((m) => m.content);
412
+ const contents = handler.messages.value.map((m) => getText(m.content));
345
413
  expect(contents).toEqual(['One', 'Reply 1', 'Two', 'Reply 2']);
346
414
  });
347
415
 
@@ -355,7 +423,7 @@ describe('useMessageHandler', () => {
355
423
 
356
424
  handler.updateMessage({
357
425
  id: 'a1',
358
- content: 'Hi',
426
+ content: [text('Hi')],
359
427
  role: 'assistant',
360
428
  status: 'completed',
361
429
  });
@@ -369,12 +437,12 @@ describe('useMessageHandler', () => {
369
437
  handler.setTree(
370
438
  makeTree(
371
439
  {
372
- u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
440
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
373
441
  a1: makeTreeMessage({
374
442
  id: 'a1',
375
443
  parentId: 'u1',
376
444
  role: 'assistant',
377
- content: 'Hi',
445
+ content: [text('Hi')],
378
446
  }),
379
447
  },
380
448
  'u1',
@@ -395,13 +463,13 @@ describe('useMessageHandler', () => {
395
463
  id: 'a',
396
464
  parentId: 'r',
397
465
  role: 'assistant',
398
- content: 'A',
466
+ content: [text('A')],
399
467
  }),
400
468
  b: makeTreeMessage({
401
469
  id: 'b',
402
470
  parentId: 'r',
403
471
  role: 'assistant',
404
- content: 'B',
472
+ content: [text('B')],
405
473
  }),
406
474
  },
407
475
  'r',
@@ -419,23 +487,23 @@ describe('useMessageHandler', () => {
419
487
  handler.setTree(
420
488
  makeTree(
421
489
  {
422
- u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
490
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
423
491
  a1: makeTreeMessage({
424
492
  id: 'a1',
425
493
  parentId: 'u1',
426
494
  role: 'assistant',
427
- content: 'Hi',
495
+ content: [text('Hi')],
428
496
  }),
429
497
  a2: makeTreeMessage({
430
498
  id: 'a2',
431
499
  parentId: 'u1',
432
500
  role: 'assistant',
433
- content: 'Hey',
501
+ content: [text('Hey')],
434
502
  }),
435
503
  u2: makeTreeMessage({
436
504
  id: 'u2',
437
505
  parentId: 'a2',
438
- content: 'Follow-up',
506
+ content: [text('Follow-up')],
439
507
  }),
440
508
  },
441
509
  'u1',
@@ -459,18 +527,18 @@ describe('useMessageHandler', () => {
459
527
  handler.setTree(
460
528
  makeTree(
461
529
  {
462
- u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
530
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
463
531
  a1: makeTreeMessage({
464
532
  id: 'a1',
465
533
  parentId: 'u1',
466
534
  role: 'assistant',
467
- content: 'Hi',
535
+ content: [text('Hi')],
468
536
  }),
469
537
  a2: makeTreeMessage({
470
538
  id: 'a2',
471
539
  parentId: 'u1',
472
540
  role: 'assistant',
473
- content: 'Hey',
541
+ content: [text('Hey')],
474
542
  }),
475
543
  },
476
544
  'u1',
@@ -502,23 +570,23 @@ describe('useMessageHandler', () => {
502
570
  handler.setTree(
503
571
  makeTree(
504
572
  {
505
- u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
573
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
506
574
  a1: makeTreeMessage({
507
575
  id: 'a1',
508
576
  parentId: 'u1',
509
577
  role: 'assistant',
510
- content: 'Hi',
578
+ content: [text('Hi')],
511
579
  }),
512
580
  a2: makeTreeMessage({
513
581
  id: 'a2',
514
582
  parentId: 'u1',
515
583
  role: 'assistant',
516
- content: 'Hey',
584
+ content: [text('Hey')],
517
585
  }),
518
586
  u2: makeTreeMessage({
519
587
  id: 'u2',
520
588
  parentId: 'a2',
521
- content: 'Follow-up',
589
+ content: [text('Follow-up')],
522
590
  }),
523
591
  },
524
592
  'u1',
@@ -10,6 +10,7 @@ import {
10
10
  import type {
11
11
  BaseMessage,
12
12
  ConversationTree,
13
+ MessageContent,
13
14
  MessageRole,
14
15
  MessageStatus,
15
16
  TreeMessage,
@@ -40,13 +41,17 @@ export const useMessageHandler = () => {
40
41
  const text = currentUserMessage.value.trim();
41
42
  if (!text) throw new Error('No message');
42
43
 
44
+ const content: MessageContent[] = [
45
+ { type: 'text', id: generateId(), text, status: 'completed' },
46
+ ];
47
+
43
48
  const node: TreeMessage = {
44
49
  id,
45
50
  parentId: currentLeafId.value,
46
51
  childrenIds: [],
47
52
  activeChildIndex: 0,
48
53
  role: 'user',
49
- content: text,
54
+ content,
50
55
  status: 'completed',
51
56
  createdAt: new Date(),
52
57
  };
@@ -65,7 +70,7 @@ export const useMessageHandler = () => {
65
70
 
66
71
  function updateMessage(message: {
67
72
  id: string;
68
- content: string;
73
+ content: MessageContent[];
69
74
  role: MessageRole;
70
75
  status: MessageStatus;
71
76
  }) {
@@ -112,6 +117,15 @@ export const useMessageHandler = () => {
112
117
  Object.assign(tree, createEmptyTree());
113
118
  }
114
119
 
120
+ /**
121
+ * Replace the entire tree. Pair with a format adapter (e.g.
122
+ * `responsesToTree` from `@aleph-alpha/chat-kit/adapters/responses-api`)
123
+ * when hydrating from a server transcript:
124
+ *
125
+ * ```ts
126
+ * setTree(responsesToTree(await fetchResponses(id)));
127
+ * ```
128
+ */
115
129
  function setTree(incoming: ConversationTree) {
116
130
  tree.nodes = incoming.nodes;
117
131
  tree.rootId = incoming.rootId;