@aleph-alpha/chat-kit 1.3.1 → 1.5.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.
- package/dist/{CkInput.vue_vue_type_script_setup_true_lang-CyGLItQZ.js → CkInput.vue_vue_type_script_setup_true_lang-C5C09u0z.js} +9 -8
- package/dist/adapters/index.d.ts +18 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/responses-api.d.ts +180 -0
- package/dist/adapters/responses-api.d.ts.map +1 -0
- package/dist/adapters/responses-api.js +153 -0
- package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts +1 -1
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +11 -3
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +5 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/composables/fileUpload/index.d.ts +7 -0
- package/dist/composables/fileUpload/index.d.ts.map +1 -0
- package/dist/composables/fileUpload/types.d.ts +295 -0
- package/dist/composables/fileUpload/types.d.ts.map +1 -0
- package/dist/composables/fileUpload/useFileUpload.d.ts +4 -0
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -0
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts +3 -0
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -0
- package/dist/composables/index.d.ts +2 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/index.js +5 -2
- package/dist/composables/useConversations.d.ts +12 -0
- package/dist/composables/useConversations.d.ts.map +1 -1
- package/dist/generateId-BHf0W4Rq.js +6 -0
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useMessageHandler.d.ts +2 -2
- package/dist/handlers/useMessageHandler.d.ts.map +1 -1
- package/dist/index-BbTgIoYa.js +1557 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -8
- package/dist/services/index.d.ts +0 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -37
- package/dist/services/types.d.ts +2 -2
- package/dist/services/types.d.ts.map +1 -1
- package/dist/types.d.ts +47 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/{useMessageHandler-BmBdu2EW.js → useMessageHandler-ZRC-gIOJ.js} +5 -5
- package/package.json +10 -2
- package/src/adapters/index.ts +17 -0
- package/src/adapters/responses-api.spec.ts +365 -0
- package/src/adapters/responses-api.ts +444 -0
- package/src/components/composed/CkConversation/CkConversation.spec.ts +171 -25
- package/src/components/composed/CkConversation/CkConversation.stories.ts +266 -46
- package/src/components/composed/CkConversation/CkConversation.vue +19 -7
- package/src/composables/fileUpload/index.spec.ts +213 -0
- package/src/composables/fileUpload/index.ts +83 -0
- package/src/composables/fileUpload/types.ts +409 -0
- package/src/composables/fileUpload/useFileUpload.ts +1242 -0
- package/src/composables/fileUpload/useFileUploadWrapper.ts +382 -0
- package/src/composables/index.ts +2 -1
- package/src/composables/useConversations.spec.ts +118 -0
- package/src/composables/useConversations.ts +32 -0
- package/src/handlers/useChatKitLabels.spec.ts +16 -2
- package/src/handlers/useMessageHandler.spec.ts +112 -44
- package/src/handlers/useMessageHandler.ts +16 -2
- package/src/helpers/conversationTree.spec.ts +46 -19
- package/src/index.ts +11 -2
- package/src/services/index.ts +0 -2
- package/src/services/types.ts +2 -2
- package/src/types.ts +55 -2
- package/dist/ChatService-LogwJNxh.js +0 -9
- package/dist/services/ChatService.d.ts +0 -42
- package/dist/services/ChatService.d.ts.map +0 -1
- package/dist/services/MockChatService.d.ts +0 -10
- package/dist/services/MockChatService.d.ts.map +0 -1
- package/dist/useConversationSessions-ulFhtpQr.js +0 -243
- package/src/services/ChatService.ts +0 -60
- package/src/services/MockChatService.ts +0 -47
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: OpenAI-style **Responses API** → chat-kit `ConversationTree`.
|
|
3
|
+
*
|
|
4
|
+
* chat-kit's domain model (`TreeMessage`, `MessageContent`) is intentionally
|
|
5
|
+
* format-agnostic. This module is the bridge that turns a stored Responses-
|
|
6
|
+
* API timeline (`GET /conversations/{id}/responses`) into a `ConversationTree`
|
|
7
|
+
* the message handler can render. Apps that use a different transcript shape
|
|
8
|
+
* — or none at all — can build their own adapter or feed the handler via
|
|
9
|
+
* `setTree(tree)` directly.
|
|
10
|
+
*
|
|
11
|
+
* The wire-level types that describe the Responses-API payload (`StoredResponse`,
|
|
12
|
+
* `ResponseOutput*`, `ResponseAttachment`, `ResponseList`, …) live alongside the
|
|
13
|
+
* conversion function so the entire Responses-API surface is encapsulated here.
|
|
14
|
+
*
|
|
15
|
+
* Public surface:
|
|
16
|
+
* - `responsesToTree(responses)` — pure function, the entry point.
|
|
17
|
+
* - All Responses-API wire types (re-exported via the adapter subpath).
|
|
18
|
+
*
|
|
19
|
+
* Available via `@aleph-alpha/chat-kit/adapters/responses-api` and
|
|
20
|
+
* `@aleph-alpha/chat-kit/adapters`.
|
|
21
|
+
*/
|
|
22
|
+
import { generateId } from '../helpers';
|
|
23
|
+
import type {
|
|
24
|
+
ConversationTree,
|
|
25
|
+
MessageContent,
|
|
26
|
+
MessageRole,
|
|
27
|
+
TreeMessage,
|
|
28
|
+
} from '../types';
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Wire-level Responses-API types
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Annotation attached to a Responses-API content part (citations, file
|
|
36
|
+
* references, etc.). chat-kit does not render annotations itself; the shape
|
|
37
|
+
* is kept open so consumers can downcast to a more specific schema.
|
|
38
|
+
*/
|
|
39
|
+
export interface ResponseAnnotation {
|
|
40
|
+
type: string;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Plain text fragment inside a `ResponseOutputMessage`. Streamed by the
|
|
46
|
+
* Responses API as `output_text` (or the historical `text` alias).
|
|
47
|
+
*/
|
|
48
|
+
export interface ResponseOutputTextPart {
|
|
49
|
+
type: 'output_text' | 'text';
|
|
50
|
+
text: string;
|
|
51
|
+
annotations?: ResponseAnnotation[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One reasoning fragment inside a `ResponseOutputReasoning`. The Responses
|
|
56
|
+
* API streams chain-of-thought under `reasoning_text`; the body is exposed
|
|
57
|
+
* for consumers that want to render it (chat-kit itself only surfaces a
|
|
58
|
+
* "thinking" indicator).
|
|
59
|
+
*/
|
|
60
|
+
export interface ResponseReasoningTextPart {
|
|
61
|
+
type: 'reasoning_text';
|
|
62
|
+
text: string;
|
|
63
|
+
annotations?: ResponseAnnotation[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Forward-compatible catch-all for content parts whose `type` chat-kit does
|
|
68
|
+
* not yet model (e.g. refusals, future part kinds). Consumers narrow on
|
|
69
|
+
* `type` themselves when they need richer handling.
|
|
70
|
+
*/
|
|
71
|
+
export interface ResponseUnknownContentPart {
|
|
72
|
+
type: string;
|
|
73
|
+
text?: string;
|
|
74
|
+
annotations?: ResponseAnnotation[];
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Discriminated union of content parts that can appear inside a Responses-
|
|
80
|
+
* API output item. `ResponseOutputTextPart` and `ResponseReasoningTextPart`
|
|
81
|
+
* cover the rendered shapes; the catch-all keeps the union open.
|
|
82
|
+
*/
|
|
83
|
+
export type OutputContentPart =
|
|
84
|
+
| ResponseOutputTextPart
|
|
85
|
+
| ResponseReasoningTextPart
|
|
86
|
+
| ResponseUnknownContentPart;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Visible message produced by a Responses-API turn (assistant reply or
|
|
90
|
+
* echoed user input). Carries the renderable text content; `responsesToTree`
|
|
91
|
+
* extracts text from `content` to build the chat tree.
|
|
92
|
+
*/
|
|
93
|
+
export interface ResponseOutputMessage {
|
|
94
|
+
type: 'message';
|
|
95
|
+
id: string;
|
|
96
|
+
status: string;
|
|
97
|
+
role: 'assistant' | 'user';
|
|
98
|
+
content: string | OutputContentPart[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Chain-of-thought trail produced by the assistant during a turn. Surfaced
|
|
103
|
+
* in chat-kit only as a "thinking" indicator; consumers may render the
|
|
104
|
+
* underlying `reasoning_text` parts directly when desired.
|
|
105
|
+
*/
|
|
106
|
+
export interface ResponseOutputReasoning {
|
|
107
|
+
type: 'reasoning';
|
|
108
|
+
id: string;
|
|
109
|
+
status: string;
|
|
110
|
+
role?: 'assistant' | null;
|
|
111
|
+
content: OutputContentPart[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Single tool entry returned inside `mcp_list_tools` (and similar tool-
|
|
116
|
+
* discovery outputs). Schema fields are passed through verbatim because
|
|
117
|
+
* chat-kit does not model JSON Schema itself.
|
|
118
|
+
*/
|
|
119
|
+
export interface ResponseToolDescriptor {
|
|
120
|
+
name: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
annotations?: Record<string, unknown>;
|
|
123
|
+
input_schema?: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Wire-level Responses-API output kinds that represent tool activity rather
|
|
128
|
+
* than visible content. The list enumerates the values chat-kit recognises
|
|
129
|
+
* today; extend it as new tool kinds are introduced upstream.
|
|
130
|
+
*/
|
|
131
|
+
export type ResponseOutputToolType =
|
|
132
|
+
| 'mcp_list_tools'
|
|
133
|
+
| 'mcp_call'
|
|
134
|
+
| 'tool_call'
|
|
135
|
+
| 'function_call'
|
|
136
|
+
| 'web_search_call'
|
|
137
|
+
| 'file_search_call'
|
|
138
|
+
| 'code_interpreter_call'
|
|
139
|
+
| 'image_generation_call'
|
|
140
|
+
| 'local_shell_call';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Tool-related output produced during a Responses-API turn. Covers tool
|
|
144
|
+
* discovery (`mcp_list_tools`), invocation (`mcp_call`, `function_call`,
|
|
145
|
+
* etc.) and the various built-in tool kinds. Common fields are typed; the
|
|
146
|
+
* rest is passed through so consumers can read kind-specific data without
|
|
147
|
+
* redefining the shape.
|
|
148
|
+
*/
|
|
149
|
+
export interface ResponseOutputTool {
|
|
150
|
+
type: ResponseOutputToolType;
|
|
151
|
+
id: string;
|
|
152
|
+
status?: string;
|
|
153
|
+
/** `mcp_list_tools` / `mcp_call`: server identifier. */
|
|
154
|
+
server_label?: string;
|
|
155
|
+
/** `mcp_list_tools`: catalog of tools exposed by the server. */
|
|
156
|
+
tools?: ResponseToolDescriptor[];
|
|
157
|
+
/** `function_call` / `mcp_call`: invoked tool name. */
|
|
158
|
+
name?: string;
|
|
159
|
+
/** `function_call` / `mcp_call`: serialized JSON arguments. */
|
|
160
|
+
arguments?: string;
|
|
161
|
+
/** `function_call` / `tool_call`: correlation id for a paired result. */
|
|
162
|
+
call_id?: string;
|
|
163
|
+
/** Tool result payload (shape depends on the tool kind). */
|
|
164
|
+
output?: unknown;
|
|
165
|
+
[key: string]: unknown;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* One output item produced by a Responses-API response, discriminated by
|
|
170
|
+
* `type`. Modelled in the same spirit as `MessageContent`: visible
|
|
171
|
+
* `'message'`, optional `'reasoning'` trail, and `tool` activity.
|
|
172
|
+
*/
|
|
173
|
+
export type ResponseOutput =
|
|
174
|
+
| ResponseOutputMessage
|
|
175
|
+
| ResponseOutputReasoning
|
|
176
|
+
| ResponseOutputTool;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Attachment metadata stored under `metadata.attachments` on a Responses-API
|
|
180
|
+
* response. chat-kit does not render attachments itself; this type exists so
|
|
181
|
+
* consumers can read them off `StoredResponse.metadata` without redeclaring
|
|
182
|
+
* the shape.
|
|
183
|
+
*/
|
|
184
|
+
export interface ResponseAttachment {
|
|
185
|
+
fileId: string;
|
|
186
|
+
fileName: string;
|
|
187
|
+
searchStoreId: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* One stored response from the OpenAI-style Responses API, as returned by
|
|
192
|
+
* `GET /conversations/:id/responses`. `previous_response_id` chains
|
|
193
|
+
* responses into a tree; `responsesToTree` walks that chain to produce a
|
|
194
|
+
* `ConversationTree`.
|
|
195
|
+
*/
|
|
196
|
+
export interface StoredResponse {
|
|
197
|
+
id: string;
|
|
198
|
+
object: 'response';
|
|
199
|
+
created_at: number;
|
|
200
|
+
completed_at: number;
|
|
201
|
+
status: string;
|
|
202
|
+
model: string;
|
|
203
|
+
previous_response_id?: string | null;
|
|
204
|
+
output: ResponseOutput[];
|
|
205
|
+
request_input: Record<string, unknown>[];
|
|
206
|
+
metadata?: Record<string, unknown> & {
|
|
207
|
+
attachments?: ResponseAttachment[];
|
|
208
|
+
};
|
|
209
|
+
conversation?: { id: string };
|
|
210
|
+
ancestor_ids?: string[];
|
|
211
|
+
depth?: number;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Raw envelope returned by the Responses list endpoint. The endpoint is not
|
|
216
|
+
* paginated, so chat-kit unwraps `data` and exposes `StoredResponse[]` to
|
|
217
|
+
* callers; this type is kept exported for users implementing or mocking the
|
|
218
|
+
* `HttpClient`.
|
|
219
|
+
*/
|
|
220
|
+
export interface ResponseList {
|
|
221
|
+
object: 'list';
|
|
222
|
+
data: StoredResponse[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Conversion: StoredResponse[] → ConversationTree
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
interface ResponseMessageNode {
|
|
230
|
+
id: string;
|
|
231
|
+
role: MessageRole;
|
|
232
|
+
content: MessageContent[];
|
|
233
|
+
createdAt: Date;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function extractMessageText(content: ResponseOutputMessage['content']): string {
|
|
237
|
+
if (typeof content === 'string') return content.trim();
|
|
238
|
+
return extractTextFromParts(content);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function extractTextFromParts(parts: OutputContentPart[]): string {
|
|
242
|
+
let text = '';
|
|
243
|
+
for (const part of parts) {
|
|
244
|
+
if (part.type === 'output_text' || part.type === 'text') {
|
|
245
|
+
text += part.text ?? '';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return text.trim();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function extractReasoningText(parts: OutputContentPart[]): string {
|
|
252
|
+
let text = '';
|
|
253
|
+
for (const part of parts) {
|
|
254
|
+
if (part.type === 'reasoning_text') {
|
|
255
|
+
text += part.text ?? '';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return text.trim();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function textPart(text: string, id?: string): MessageContent {
|
|
262
|
+
return { type: 'text', id: id ?? generateId(), text, status: 'completed' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Build the `content` array for one assistant turn from a Responses-API
|
|
266
|
+
// record. Reasoning trails, tool invocations, and visible text all belong to
|
|
267
|
+
// the same turn and are kept in their original wire order so the UI can
|
|
268
|
+
// render them inline with the streaming flow.
|
|
269
|
+
function toAssistantParts(resp: StoredResponse): MessageContent[] {
|
|
270
|
+
const parts: MessageContent[] = [];
|
|
271
|
+
for (const out of resp.output ?? []) {
|
|
272
|
+
switch (out.type) {
|
|
273
|
+
case 'reasoning': {
|
|
274
|
+
const text = extractReasoningText(out.content);
|
|
275
|
+
parts.push({
|
|
276
|
+
type: 'reasoning',
|
|
277
|
+
id: out.id,
|
|
278
|
+
...(text ? { text } : {}),
|
|
279
|
+
status: 'completed',
|
|
280
|
+
});
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case 'message': {
|
|
284
|
+
if (out.role !== 'assistant') break;
|
|
285
|
+
const text = extractMessageText(out.content);
|
|
286
|
+
if (!text) break;
|
|
287
|
+
parts.push(textPart(text, out.id || undefined));
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
// Actual tool invocations surface as tool parts. `mcp_list_tools` is
|
|
291
|
+
// informational discovery (the server advertising tools) rather than
|
|
292
|
+
// a call the assistant made, so it is intentionally skipped.
|
|
293
|
+
case 'mcp_call':
|
|
294
|
+
case 'function_call':
|
|
295
|
+
case 'tool_call': {
|
|
296
|
+
parts.push({
|
|
297
|
+
type: 'tool',
|
|
298
|
+
id: out.id,
|
|
299
|
+
name: out.name ?? out.type,
|
|
300
|
+
});
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
default:
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return parts;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The id used for the assistant `TreeMessage` of a response. Prefer the id
|
|
311
|
+
// of the first visible message output (matches the wire id seen during
|
|
312
|
+
// streaming); fall back to a deterministic `assistant-${resp.id}` so
|
|
313
|
+
// reasoning- or tool-only turns still get a stable node id.
|
|
314
|
+
function getAssistantNodeId(resp: StoredResponse): string {
|
|
315
|
+
for (const out of resp.output ?? []) {
|
|
316
|
+
if (out.type === 'message' && out.role === 'assistant' && out.id) {
|
|
317
|
+
return out.id;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return `assistant-${resp.id}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Within a single response, the visible messages chain linearly: every user
|
|
324
|
+
// input is followed by exactly one assistant turn (whose reasoning, tool,
|
|
325
|
+
// and text outputs are merged into one node). Across responses, branching
|
|
326
|
+
// is encoded by `previous_response_id` so several responses can share the
|
|
327
|
+
// same parent and form sibling subtrees.
|
|
328
|
+
function extractInputText(rawContent: unknown): string {
|
|
329
|
+
if (typeof rawContent === 'string') return rawContent.trim();
|
|
330
|
+
if (Array.isArray(rawContent)) {
|
|
331
|
+
return extractTextFromParts(rawContent as OutputContentPart[]);
|
|
332
|
+
}
|
|
333
|
+
return '';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function responseToMessageNodes(resp: StoredResponse): ResponseMessageNode[] {
|
|
337
|
+
const nodes: ResponseMessageNode[] = [];
|
|
338
|
+
if (resp.request_input) {
|
|
339
|
+
for (const [idx, input] of resp.request_input.entries()) {
|
|
340
|
+
const role = (input as { role?: string }).role;
|
|
341
|
+
const rawContent = (input as { content?: unknown }).content;
|
|
342
|
+
const text = extractInputText(rawContent);
|
|
343
|
+
if (role === 'user' && text) {
|
|
344
|
+
nodes.push({
|
|
345
|
+
id: `user-${resp.id}-${idx}`,
|
|
346
|
+
role: 'user',
|
|
347
|
+
content: [textPart(text)],
|
|
348
|
+
createdAt: new Date(resp.created_at * 1000),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const assistantParts = toAssistantParts(resp);
|
|
354
|
+
if (assistantParts.length > 0) {
|
|
355
|
+
nodes.push({
|
|
356
|
+
id: getAssistantNodeId(resp),
|
|
357
|
+
role: 'assistant',
|
|
358
|
+
content: assistantParts,
|
|
359
|
+
createdAt: new Date((resp.completed_at ?? resp.created_at) * 1000),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return nodes;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Convert an OpenAI Responses API list (already ordered by `created_at`
|
|
367
|
+
* ascending) into a chat-kit `ConversationTree`.
|
|
368
|
+
*
|
|
369
|
+
* The caller guarantees ordering, which lets us build the tree in a single
|
|
370
|
+
* pass: each response's `previous_response_id` always points to a response
|
|
371
|
+
* we have already processed, so we can wire its messages onto the parent's
|
|
372
|
+
* last message immediately. Sibling responses (same `previous_response_id`)
|
|
373
|
+
* surface as branches; the latest one wins as the active branch.
|
|
374
|
+
*
|
|
375
|
+
* Pair with `useMessageHandler().setTree(responsesToTree(responses))` when
|
|
376
|
+
* you want to hydrate a handler.
|
|
377
|
+
*/
|
|
378
|
+
export function responsesToTree(responses: StoredResponse[]): ConversationTree {
|
|
379
|
+
if (responses.length === 0) {
|
|
380
|
+
return { nodes: {}, rootId: null };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const treeNodes: Record<string, TreeMessage> = {};
|
|
384
|
+
// For each response, the id of its last visible message — that's the
|
|
385
|
+
// attachment point for any future child response that chains onto it.
|
|
386
|
+
// Empty responses fall back to their parent's attachment point so the
|
|
387
|
+
// chain stays connected.
|
|
388
|
+
const tailMessageByResponse = new Map<string, string | null>();
|
|
389
|
+
let rootMessageId: string | null = null;
|
|
390
|
+
|
|
391
|
+
function appendNode(node: ResponseMessageNode, parentId: string | null) {
|
|
392
|
+
treeNodes[node.id] = {
|
|
393
|
+
id: node.id,
|
|
394
|
+
parentId,
|
|
395
|
+
childrenIds: [],
|
|
396
|
+
activeChildIndex: 0,
|
|
397
|
+
role: node.role,
|
|
398
|
+
content: node.content,
|
|
399
|
+
status: 'completed',
|
|
400
|
+
createdAt: node.createdAt,
|
|
401
|
+
};
|
|
402
|
+
if (parentId !== null) {
|
|
403
|
+
const parent = treeNodes[parentId];
|
|
404
|
+
if (parent) {
|
|
405
|
+
parent.childrenIds.push(node.id);
|
|
406
|
+
// Latest insertion wins as the active branch — within a single
|
|
407
|
+
// response there's only one push, so this is a no-op there; for
|
|
408
|
+
// sibling branches it surfaces the most recent one by default.
|
|
409
|
+
parent.activeChildIndex = parent.childrenIds.length - 1;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const resp of responses) {
|
|
415
|
+
const parentResponseId =
|
|
416
|
+
resp.previous_response_id &&
|
|
417
|
+
tailMessageByResponse.has(resp.previous_response_id)
|
|
418
|
+
? resp.previous_response_id
|
|
419
|
+
: null;
|
|
420
|
+
let chainParent =
|
|
421
|
+
parentResponseId !== null
|
|
422
|
+
? (tailMessageByResponse.get(parentResponseId) ?? null)
|
|
423
|
+
: null;
|
|
424
|
+
|
|
425
|
+
const messageNodes = responseToMessageNodes(resp);
|
|
426
|
+
let firstId: string | null = null;
|
|
427
|
+
for (const node of messageNodes) {
|
|
428
|
+
appendNode(node, chainParent);
|
|
429
|
+
if (firstId === null) firstId = node.id;
|
|
430
|
+
chainParent = node.id;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (rootMessageId === null && firstId !== null) {
|
|
434
|
+
rootMessageId = firstId;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Track the attachment point for descendants. If the response had no
|
|
438
|
+
// visible messages, pass through the parent's tail so children of this
|
|
439
|
+
// response still land in the right place.
|
|
440
|
+
tailMessageByResponse.set(resp.id, chainParent);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { nodes: treeNodes, rootId: rootMessageId };
|
|
444
|
+
}
|