@aleph-alpha/chat-kit 3.3.0 → 4.0.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-BAcBFs6I.js → CkInput.vue_vue_type_script_setup_true_lang-DJN-nZ2-.js} +118 -71
- package/dist/adapters/responses-api.d.ts +11 -5
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +9 -3
- package/dist/chat-kit.css +8 -8
- package/dist/components/base/CkAgentMessage/types.d.ts +1 -1
- package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts → CkCopyMessageButton/CkCopyMessageButton.stories.d.ts} +3 -3
- package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts → CkCopyMessageButton/CkCopyMessageButton.vue.d.ts} +4 -4
- package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkCopyMessageButton/index.d.ts +3 -0
- package/dist/components/base/CkCopyMessageButton/index.d.ts.map +1 -0
- package/dist/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.d.ts +3 -3
- package/dist/components/base/CkCopyMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackButton/types.d.ts +1 -1
- package/dist/components/base/CkFeedbackDialog/types.d.ts +1 -1
- package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.stories.d.ts → CkMessageButton/CkMessageButton.stories.d.ts} +3 -3
- package/dist/components/base/CkMessageButton/CkMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.vue.d.ts → CkMessageButton/CkMessageButton.vue.d.ts} +3 -3
- package/dist/components/base/CkMessageButton/CkMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkMessageButton/index.d.ts +3 -0
- package/dist/components/base/CkMessageButton/index.d.ts.map +1 -0
- package/dist/components/base/{CkMessageActionButton → CkMessageButton}/types.d.ts +2 -2
- package/dist/components/base/CkMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts +16 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts +12 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkRegenerateMessageButton/index.d.ts +3 -0
- package/dist/components/base/{CkCopyMessageActionButton → CkRegenerateMessageButton}/index.d.ts.map +1 -1
- package/dist/components/base/CkRegenerateMessageButton/types.d.ts +27 -0
- package/dist/components/base/CkRegenerateMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/index.d.ts +3 -2
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +1 -1
- package/dist/components/composed/CkConversation/types.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +7 -6
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.js +2 -2
- package/dist/handlers/useChatKitLabels.d.ts +2 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/handlers/useMessageHandler.d.ts +37 -0
- package/dist/handlers/useMessageHandler.d.ts.map +1 -1
- package/dist/helpers/conversationTree.d.ts +15 -0
- package/dist/helpers/conversationTree.d.ts.map +1 -1
- package/dist/{index-C6njJ8EB.js → index-CyrVnbS9.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -10
- package/dist/{useChatKitLabels-BM2I7-9o.js → useChatKitLabels-zMFV7aEz.js} +1 -0
- package/dist/{useMessageHandler-CMdy-lm0.js → useMessageHandler-N50Yj-NU.js} +45 -7
- package/package.json +3 -3
- package/src/adapters/responses-api.spec.ts +52 -5
- package/src/adapters/responses-api.ts +26 -7
- package/src/components/base/CkAgentMessage/types.ts +1 -1
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts → CkCopyMessageButton/CkCopyMessageButton.spec.ts} +6 -6
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts → CkCopyMessageButton/CkCopyMessageButton.stories.ts} +8 -8
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue → CkCopyMessageButton/CkCopyMessageButton.vue} +6 -6
- package/src/components/base/CkCopyMessageButton/index.ts +2 -0
- package/src/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.ts +3 -3
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +5 -5
- package/src/components/base/CkFeedbackButton/types.ts +1 -1
- package/src/components/base/CkFeedbackDialog/types.ts +1 -1
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.spec.ts → CkMessageButton/CkMessageButton.spec.ts} +6 -6
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.stories.ts → CkMessageButton/CkMessageButton.stories.ts} +10 -10
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.vue → CkMessageButton/CkMessageButton.vue} +3 -3
- package/src/components/base/CkMessageButton/index.ts +2 -0
- package/src/components/base/{CkMessageActionButton → CkMessageButton}/types.ts +2 -2
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.spec.ts +49 -0
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.ts +63 -0
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue +43 -0
- package/src/components/base/CkRegenerateMessageButton/index.ts +2 -0
- package/src/components/base/CkRegenerateMessageButton/types.ts +27 -0
- package/src/components/base/index.ts +3 -2
- package/src/components/composed/CkConversation/CkConversation.stories.ts +6 -6
- package/src/components/composed/CkConversation/CkConversation.vue +2 -2
- package/src/components/composed/CkConversation/types.ts +1 -1
- package/src/components/index.ts +3 -2
- package/src/handlers/useChatKitLabels.spec.ts +1 -0
- package/src/handlers/useChatKitLabels.ts +3 -0
- package/src/handlers/useMessageHandler.spec.ts +79 -0
- package/src/handlers/useMessageHandler.ts +50 -2
- package/src/helpers/conversationTree.spec.ts +120 -0
- package/src/helpers/conversationTree.ts +57 -8
- package/src/index.ts +3 -2
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts.map +0 -1
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts.map +0 -1
- package/dist/components/base/CkCopyMessageActionButton/index.d.ts +0 -3
- package/dist/components/base/CkCopyMessageActionButton/types.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/index.d.ts +0 -3
- package/dist/components/base/CkMessageActionButton/index.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/types.d.ts.map +0 -1
- package/src/components/base/CkCopyMessageActionButton/index.ts +0 -2
- package/src/components/base/CkMessageActionButton/index.ts +0 -2
|
@@ -71,10 +71,8 @@ function updateNodeId(tree, oldId, newId) {
|
|
|
71
71
|
}
|
|
72
72
|
if (tree.rootId === oldId) tree.rootId = newId;
|
|
73
73
|
}
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
if (!node) return;
|
|
77
|
-
const stack = [...node.childrenIds];
|
|
74
|
+
function deleteSubtrees(tree, ids) {
|
|
75
|
+
const stack = [...ids];
|
|
78
76
|
while (stack.length > 0) {
|
|
79
77
|
const currentId = stack.pop();
|
|
80
78
|
const current = tree.nodes[currentId];
|
|
@@ -82,9 +80,33 @@ function cutBranchAfter(tree, id) {
|
|
|
82
80
|
stack.push(...current.childrenIds);
|
|
83
81
|
delete tree.nodes[currentId];
|
|
84
82
|
}
|
|
83
|
+
}
|
|
84
|
+
function cutBranchAfter(tree, id) {
|
|
85
|
+
const node = tree.nodes[id];
|
|
86
|
+
if (!node) return;
|
|
87
|
+
deleteSubtrees(tree, node.childrenIds);
|
|
85
88
|
node.childrenIds = [];
|
|
86
89
|
node.activeChildIndex = 0;
|
|
87
90
|
}
|
|
91
|
+
function cutBranchAt(tree, id) {
|
|
92
|
+
const node = tree.nodes[id];
|
|
93
|
+
if (!node) return;
|
|
94
|
+
deleteSubtrees(tree, [id]);
|
|
95
|
+
if (node.parentId === null) {
|
|
96
|
+
if (tree.rootId === id) tree.rootId = null;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const parent = tree.nodes[node.parentId];
|
|
100
|
+
if (!parent) return;
|
|
101
|
+
const idx = parent.childrenIds.indexOf(id);
|
|
102
|
+
if (idx !== -1) {
|
|
103
|
+
parent.childrenIds.splice(idx, 1);
|
|
104
|
+
if (idx < parent.activeChildIndex) parent.activeChildIndex -= 1;
|
|
105
|
+
}
|
|
106
|
+
if (parent.activeChildIndex >= parent.childrenIds.length) {
|
|
107
|
+
parent.activeChildIndex = Math.max(0, parent.childrenIds.length - 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
88
110
|
function navigate(tree, targetId) {
|
|
89
111
|
const node = tree.nodes[targetId];
|
|
90
112
|
if (!node || node.parentId === null) return;
|
|
@@ -102,6 +124,13 @@ const useMessageHandler = () => {
|
|
|
102
124
|
const list = messages.value;
|
|
103
125
|
return list.length > 0 ? list[list.length - 1].id : null;
|
|
104
126
|
});
|
|
127
|
+
function resolveParentId(parentId) {
|
|
128
|
+
if (parentId === void 0) return currentLeafId.value;
|
|
129
|
+
if (!tree.nodes[parentId]) {
|
|
130
|
+
throw new Error(`Unknown parent message id: ${parentId}`);
|
|
131
|
+
}
|
|
132
|
+
return parentId;
|
|
133
|
+
}
|
|
105
134
|
function setCurrentUserMessage(text) {
|
|
106
135
|
currentUserMessage.value = text;
|
|
107
136
|
}
|
|
@@ -112,12 +141,13 @@ const useMessageHandler = () => {
|
|
|
112
141
|
const id = generateId();
|
|
113
142
|
const text = currentUserMessage.value.trim();
|
|
114
143
|
if (!text) throw new Error("No message");
|
|
144
|
+
const parentId = resolveParentId(opts?.parentId);
|
|
115
145
|
const content = [
|
|
116
146
|
{ type: "text", id: generateId(), text, status: "completed" }
|
|
117
147
|
];
|
|
118
148
|
const node = {
|
|
119
149
|
id,
|
|
120
|
-
parentId
|
|
150
|
+
parentId,
|
|
121
151
|
childrenIds: [],
|
|
122
152
|
activeChildIndex: 0,
|
|
123
153
|
role: "user",
|
|
@@ -150,7 +180,7 @@ const useMessageHandler = () => {
|
|
|
150
180
|
} else {
|
|
151
181
|
const node = {
|
|
152
182
|
id: message.id,
|
|
153
|
-
parentId:
|
|
183
|
+
parentId: resolveParentId(message.parentId),
|
|
154
184
|
childrenIds: [],
|
|
155
185
|
activeChildIndex: 0,
|
|
156
186
|
role: message.role,
|
|
@@ -191,6 +221,12 @@ const useMessageHandler = () => {
|
|
|
191
221
|
function cutBranchAfter$1(messageId) {
|
|
192
222
|
cutBranchAfter(tree, messageId);
|
|
193
223
|
}
|
|
224
|
+
function cutBranchAt$1(messageId) {
|
|
225
|
+
cutBranchAt(tree, messageId);
|
|
226
|
+
}
|
|
227
|
+
function getTree() {
|
|
228
|
+
return readonly(tree);
|
|
229
|
+
}
|
|
194
230
|
return {
|
|
195
231
|
messages,
|
|
196
232
|
currentUserMessage: readonly(currentUserMessage),
|
|
@@ -204,7 +240,9 @@ const useMessageHandler = () => {
|
|
|
204
240
|
clearMessages,
|
|
205
241
|
setTree,
|
|
206
242
|
navigate: navigate$1,
|
|
207
|
-
cutBranchAfter: cutBranchAfter$1
|
|
243
|
+
cutBranchAfter: cutBranchAfter$1,
|
|
244
|
+
cutBranchAt: cutBranchAt$1,
|
|
245
|
+
getTree
|
|
208
246
|
};
|
|
209
247
|
};
|
|
210
248
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aleph-alpha/chat-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Chat kit package for building chat interfaces with Vue",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
"vitest": "3.2.6",
|
|
103
103
|
"vue": "^3.5.27",
|
|
104
104
|
"vue-tsc": "^3.2.4",
|
|
105
|
-
"@aleph-alpha/
|
|
105
|
+
"@aleph-alpha/eslint-config-frontend": "0.7.0",
|
|
106
106
|
"@aleph-alpha/prettier-config-frontend": "2.1.0",
|
|
107
|
-
"@aleph-alpha/
|
|
107
|
+
"@aleph-alpha/tsconfig-frontend": "0.7.0"
|
|
108
108
|
},
|
|
109
109
|
"scripts": {
|
|
110
110
|
"build": "vite build && vue-tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist",
|
|
@@ -142,6 +142,34 @@ describe('responsesToTree', () => {
|
|
|
142
142
|
expect(a1.activeChildIndex).toBe(1);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
it('favours the latest root branch when the first turn is branched into several root responses', () => {
|
|
146
|
+
const tree = responsesToTree([
|
|
147
|
+
makeResponse({
|
|
148
|
+
id: 'r1',
|
|
149
|
+
request_input: [userInput('First attempt')],
|
|
150
|
+
output: [assistantOutput('msg-a1', 'A1')],
|
|
151
|
+
}),
|
|
152
|
+
makeResponse({
|
|
153
|
+
id: 'r2',
|
|
154
|
+
request_input: [userInput('Second attempt')],
|
|
155
|
+
output: [assistantOutput('msg-a2', 'A2')],
|
|
156
|
+
}),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
// Both turns are root-anchored (no previous_response_id); the latest wins
|
|
160
|
+
// as the active root so getLinearPath surfaces it.
|
|
161
|
+
expect(tree.nodes['user-r1-0']!.parentId).toBeNull();
|
|
162
|
+
expect(tree.nodes['user-r2-0']!.parentId).toBeNull();
|
|
163
|
+
expect(tree.rootId).toBe('user-r2-0');
|
|
164
|
+
|
|
165
|
+
const handler = useMessageHandler();
|
|
166
|
+
handler.setTree(tree);
|
|
167
|
+
expect(handler.messages.value.map((m) => m.id)).toEqual([
|
|
168
|
+
'user-r2-0',
|
|
169
|
+
'msg-a2',
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
145
173
|
it('skips responses with no visible content but keeps descendants attached to the parent tail', () => {
|
|
146
174
|
const tree = responsesToTree([
|
|
147
175
|
makeResponse({
|
|
@@ -402,15 +430,31 @@ describe('responsesToTree', () => {
|
|
|
402
430
|
// string on the wire (transports flatten nested metadata).
|
|
403
431
|
metadata: {
|
|
404
432
|
attachments: JSON.stringify([
|
|
405
|
-
{
|
|
433
|
+
{
|
|
434
|
+
fileName: 'report.pdf',
|
|
435
|
+
fileId: 'f1',
|
|
436
|
+
searchStoreId: 's1',
|
|
437
|
+
documentId: 'd1',
|
|
438
|
+
},
|
|
406
439
|
]),
|
|
407
440
|
},
|
|
408
441
|
}),
|
|
409
442
|
]);
|
|
410
443
|
|
|
444
|
+
// The full attachment shape (incl. `documentId`) is preserved so the
|
|
445
|
+
// turn can be faithfully re-submitted (e.g. regenerate) after a reload.
|
|
411
446
|
expect(tree.nodes['user-r1-0']!.metadata).toEqual({
|
|
412
447
|
responseId: 'r1',
|
|
413
|
-
|
|
448
|
+
model: 'pharia-1',
|
|
449
|
+
attachments: [
|
|
450
|
+
{
|
|
451
|
+
name: 'report.pdf',
|
|
452
|
+
fileId: 'f1',
|
|
453
|
+
fileName: 'report.pdf',
|
|
454
|
+
searchStoreId: 's1',
|
|
455
|
+
documentId: 'd1',
|
|
456
|
+
},
|
|
457
|
+
],
|
|
414
458
|
});
|
|
415
459
|
// The assistant turn does not inherit attachments but still carries
|
|
416
460
|
// its responseId stamp.
|
|
@@ -431,9 +475,12 @@ describe('responsesToTree', () => {
|
|
|
431
475
|
}),
|
|
432
476
|
]);
|
|
433
477
|
|
|
434
|
-
// The malformed attachments are dropped
|
|
435
|
-
//
|
|
436
|
-
expect(tree.nodes['user-r1-0']!.metadata).toEqual({
|
|
478
|
+
// The malformed attachments are dropped; the first user node still
|
|
479
|
+
// carries `responseId` (and `model` when present).
|
|
480
|
+
expect(tree.nodes['user-r1-0']!.metadata).toEqual({
|
|
481
|
+
responseId: 'r1',
|
|
482
|
+
model: 'pharia-1',
|
|
483
|
+
});
|
|
437
484
|
expect(tree.nodes['user-r1-0']!.metadata.attachments).toBeUndefined();
|
|
438
485
|
expect(warn).toHaveBeenCalled();
|
|
439
486
|
} finally {
|
|
@@ -212,6 +212,7 @@ export interface ResponseAttachment {
|
|
|
212
212
|
fileId: string;
|
|
213
213
|
fileName: string;
|
|
214
214
|
searchStoreId: string;
|
|
215
|
+
documentId?: string;
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
/**
|
|
@@ -219,15 +220,17 @@ export interface ResponseAttachment {
|
|
|
219
220
|
* `metadata.attachments` on the user `TreeMessage` (chat-kit's `BaseMessage`
|
|
220
221
|
* /`TreeMessage` carry `metadata` as an opaque `Record<string, unknown>`).
|
|
221
222
|
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* using a different attachment shape can either skip this adapter
|
|
225
|
-
* project this onto their own type at the read site.
|
|
223
|
+
* Carries a human-readable `name` plus every id needed to resolve a public
|
|
224
|
+
* URL for the file or to re-submit it on a later turn (e.g. regenerate).
|
|
225
|
+
* Consumers using a different attachment shape can either skip this adapter
|
|
226
|
+
* or project this onto their own type at the read site.
|
|
226
227
|
*/
|
|
227
228
|
export interface ResponseMessageAttachment {
|
|
228
229
|
name: string;
|
|
229
230
|
fileId?: string;
|
|
231
|
+
fileName?: string;
|
|
230
232
|
searchStoreId?: string;
|
|
233
|
+
documentId?: string;
|
|
231
234
|
}
|
|
232
235
|
|
|
233
236
|
/**
|
|
@@ -280,6 +283,7 @@ export interface ResponseList {
|
|
|
280
283
|
*/
|
|
281
284
|
export type ResponseTreeMessageMetadata = MessageMetadata & {
|
|
282
285
|
responseId: string;
|
|
286
|
+
model?: string;
|
|
283
287
|
attachments?: ResponseMessageAttachment[];
|
|
284
288
|
};
|
|
285
289
|
|
|
@@ -478,13 +482,18 @@ function responseToMessageNodes(resp: StoredResponse): ResponseMessageNode[] {
|
|
|
478
482
|
const userContent = text || responseAttachments?.length;
|
|
479
483
|
if (role !== 'user' || !userContent) continue;
|
|
480
484
|
|
|
481
|
-
const metadata: ResponseTreeMessageMetadata = {
|
|
485
|
+
const metadata: ResponseTreeMessageMetadata = {
|
|
486
|
+
responseId: resp.id,
|
|
487
|
+
...(resp.model ? { model: resp.model } : {}),
|
|
488
|
+
};
|
|
482
489
|
if (responseAttachments && userNodesPushed === 0) {
|
|
483
490
|
metadata.attachments = responseAttachments.map(
|
|
484
491
|
(a): ResponseMessageAttachment => ({
|
|
485
492
|
name: a.fileName,
|
|
486
493
|
fileId: a.fileId,
|
|
494
|
+
fileName: a.fileName,
|
|
487
495
|
searchStoreId: a.searchStoreId,
|
|
496
|
+
...(a.documentId ? { documentId: a.documentId } : {}),
|
|
488
497
|
}),
|
|
489
498
|
);
|
|
490
499
|
}
|
|
@@ -607,6 +616,12 @@ function linkResponse(state: TreeBuildState, resp: StoredResponse): void {
|
|
|
607
616
|
parentResponseId !== null
|
|
608
617
|
? (tailMessageByResponse.get(parentResponseId) ?? null)
|
|
609
618
|
: null;
|
|
619
|
+
// Whether this response opens a new root-level branch (it chains onto
|
|
620
|
+
// nothing). The first turn can be branched/regenerated, producing several
|
|
621
|
+
// root responses; since the tree has a single `rootId` with no parent node
|
|
622
|
+
// to track sibling navigation, we keep the latest such branch as the active
|
|
623
|
+
// root — mirroring the "latest sibling wins" rule used for deeper branches.
|
|
624
|
+
const isRootChain = chainParent === null;
|
|
610
625
|
|
|
611
626
|
const messageNodes = responseToMessageNodes(resp);
|
|
612
627
|
|
|
@@ -624,7 +639,9 @@ function linkResponse(state: TreeBuildState, resp: StoredResponse): void {
|
|
|
624
639
|
chainParent = node.id;
|
|
625
640
|
}
|
|
626
641
|
|
|
627
|
-
|
|
642
|
+
// Favour the latest root branch: responses arrive in ascending `created_at`
|
|
643
|
+
// order, so a later root chain overwrites an earlier one.
|
|
644
|
+
if (firstId !== null && (state.rootMessageId === null || isRootChain)) {
|
|
628
645
|
state.rootMessageId = firstId;
|
|
629
646
|
}
|
|
630
647
|
|
|
@@ -639,7 +656,9 @@ function linkResponse(state: TreeBuildState, resp: StoredResponse): void {
|
|
|
639
656
|
* pass: each response's `previous_response_id` always points to a response
|
|
640
657
|
* we have already processed, so we can wire its messages onto the parent's
|
|
641
658
|
* last message immediately. Sibling responses (same `previous_response_id`)
|
|
642
|
-
* surface as branches; the latest one wins as the active branch.
|
|
659
|
+
* surface as branches; the latest one wins as the active branch. The same
|
|
660
|
+
* preference applies at the top level: when the first turn is branched into
|
|
661
|
+
* several root responses, the latest becomes the active `rootId`.
|
|
643
662
|
*
|
|
644
663
|
* Every emitted node carries `metadata.responseId` pointing back to the
|
|
645
664
|
* originating `StoredResponse.id` (see {@link ResponseTreeMessage}). The
|
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
* @category Messages
|
|
7
7
|
* @useCases render assistant reply, AI message layout, conversation turn, streaming message container
|
|
8
8
|
* @keywords agent, assistant, message, bubble, reply, ai, conversation
|
|
9
|
-
* @related CkUserMessage, CkConversation, CkMarkdownRenderer,
|
|
9
|
+
* @related CkUserMessage, CkConversation, CkMarkdownRenderer, CkCopyMessageButton
|
|
10
10
|
*/
|
|
11
11
|
export type CkAgentMessageProps = Record<string, never>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CkCopyMessageButton from './CkCopyMessageButton.vue';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { render, screen, waitFor } from '@testing-library/vue';
|
|
4
4
|
import { flushPromises } from '@vue/test-utils';
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
|
|
7
|
-
describe('
|
|
7
|
+
describe('CkCopyMessageButton', () => {
|
|
8
8
|
const writeText = vi.fn((_: string) => Promise.resolve());
|
|
9
9
|
|
|
10
10
|
beforeEach(() => {
|
|
@@ -14,7 +14,7 @@ describe('CkCopyMessageActionButton', () => {
|
|
|
14
14
|
|
|
15
15
|
it('emits copy with success when clipboard succeeds', async () => {
|
|
16
16
|
const user = userEvent.setup();
|
|
17
|
-
const { emitted } = render(
|
|
17
|
+
const { emitted } = render(CkCopyMessageButton, {
|
|
18
18
|
props: { text: 'x', writeToClipboard: writeText },
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -27,7 +27,7 @@ describe('CkCopyMessageActionButton', () => {
|
|
|
27
27
|
|
|
28
28
|
it('copies text and updates the accessible name', async () => {
|
|
29
29
|
const user = userEvent.setup();
|
|
30
|
-
render(
|
|
30
|
+
render(CkCopyMessageButton, {
|
|
31
31
|
props: { text: 'Hello, world', writeToClipboard: writeText },
|
|
32
32
|
});
|
|
33
33
|
|
|
@@ -50,7 +50,7 @@ describe('CkCopyMessageActionButton', () => {
|
|
|
50
50
|
|
|
51
51
|
it('emits copy with success false', async () => {
|
|
52
52
|
const user = userEvent.setup();
|
|
53
|
-
const { emitted } = render(
|
|
53
|
+
const { emitted } = render(CkCopyMessageButton, {
|
|
54
54
|
props: { text: 'x', writeToClipboard: writeText },
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -67,7 +67,7 @@ describe('CkCopyMessageActionButton', () => {
|
|
|
67
67
|
it('reverts label after durationMs', async () => {
|
|
68
68
|
vi.useFakeTimers();
|
|
69
69
|
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
|
70
|
-
render(
|
|
70
|
+
render(CkCopyMessageButton, {
|
|
71
71
|
props: { text: 'x', durationMs: 1500, writeToClipboard: writeText },
|
|
72
72
|
});
|
|
73
73
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CkCopyMessageButton from './CkCopyMessageButton.vue';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
3
3
|
|
|
4
|
-
const meta: Meta<typeof
|
|
5
|
-
title: 'Base/
|
|
6
|
-
component:
|
|
4
|
+
const meta: Meta<typeof CkCopyMessageButton> = {
|
|
5
|
+
title: 'Base/CkCopyMessageButton',
|
|
6
|
+
component: CkCopyMessageButton,
|
|
7
7
|
tags: ['autodocs'],
|
|
8
8
|
argTypes: {
|
|
9
9
|
text: {
|
|
@@ -31,19 +31,19 @@ export default meta;
|
|
|
31
31
|
type Story = StoryObj<typeof meta>;
|
|
32
32
|
|
|
33
33
|
const defaultTemplateSource = `<script setup lang="ts">
|
|
34
|
-
import {
|
|
34
|
+
import { CkCopyMessageButton } from '@aleph-alpha/chat-kit';
|
|
35
35
|
</script>
|
|
36
36
|
|
|
37
37
|
<template>
|
|
38
|
-
<
|
|
38
|
+
<CkCopyMessageButton text="Sample assistant reply text to copy." />
|
|
39
39
|
</template>`;
|
|
40
40
|
|
|
41
41
|
const disabledTemplateSource = `<script setup lang="ts">
|
|
42
|
-
import {
|
|
42
|
+
import { CkCopyMessageButton } from '@aleph-alpha/chat-kit';
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
45
|
<template>
|
|
46
|
-
<
|
|
46
|
+
<CkCopyMessageButton text="Cannot copy when disabled." disabled />
|
|
47
47
|
</template>`;
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
2
|
+
import { CkMessageButton } from '../CkMessageButton';
|
|
3
|
+
import type { CkCopyMessageButtonProps } from './types';
|
|
4
4
|
import { UiIcon, UiTooltip } from '@aleph-alpha/ui-library';
|
|
5
5
|
import { onUnmounted, ref } from 'vue';
|
|
6
6
|
|
|
7
7
|
defineOptions({
|
|
8
|
-
name: '
|
|
8
|
+
name: 'CkCopyMessageButton',
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
const props = withDefaults(defineProps<
|
|
11
|
+
const props = withDefaults(defineProps<CkCopyMessageButtonProps>(), {
|
|
12
12
|
disabled: false,
|
|
13
13
|
durationMs: 3000,
|
|
14
14
|
ariaLabelCopy: 'Copy message',
|
|
@@ -68,7 +68,7 @@ async function handleClick() {
|
|
|
68
68
|
align="center"
|
|
69
69
|
:delay-duration="0"
|
|
70
70
|
>
|
|
71
|
-
<
|
|
71
|
+
<CkMessageButton
|
|
72
72
|
v-bind="{ ariaLabel: copied ? ariaLabelCopied : ariaLabelCopy }"
|
|
73
73
|
:disabled="disabled"
|
|
74
74
|
@click="handleClick"
|
|
@@ -83,7 +83,7 @@ async function handleClick() {
|
|
|
83
83
|
/>
|
|
84
84
|
<UiIcon v-else key="check" name="check" :size="16" aria-hidden="true" />
|
|
85
85
|
</Transition>
|
|
86
|
-
</
|
|
86
|
+
</CkMessageButton>
|
|
87
87
|
<span class="sr-only" aria-live="polite">
|
|
88
88
|
{{ copied ? ariaLabelCopied : ariaLabelCopy }}
|
|
89
89
|
</span>
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* @category Actions
|
|
6
6
|
* @useCases copy assistant reply, copy message to clipboard, chat message action, feedback action on message footer
|
|
7
7
|
* @keywords copy, clipboard, action, message, button, icon, feedback
|
|
8
|
-
* @related
|
|
8
|
+
* @related CkMessageButton, CkAgentMessage, CkConversation
|
|
9
9
|
*/
|
|
10
|
-
export interface
|
|
10
|
+
export interface CkCopyMessageButtonProps {
|
|
11
11
|
/** Plain text to copy to the clipboard. */
|
|
12
12
|
text: string;
|
|
13
13
|
/**
|
|
@@ -37,7 +37,7 @@ export interface CkCopyMessageActionButtonProps {
|
|
|
37
37
|
writeToClipboard?: (text: string) => Promise<void>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export interface
|
|
40
|
+
export interface CkCopyMessageButtonEmits {
|
|
41
41
|
/** Emitted after the clipboard operation finishes (success or failure). */
|
|
42
42
|
(e: 'copy', payload: { success: boolean }): void;
|
|
43
43
|
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
defaultChatKitLabels,
|
|
4
4
|
useChatKitLabels,
|
|
5
5
|
} from '../../../handlers/useChatKitLabels';
|
|
6
|
-
import {
|
|
6
|
+
import { CkMessageButton } from '../CkMessageButton';
|
|
7
7
|
import type { CkFeedbackButtonProps, CkFeedbackValue } from './types';
|
|
8
8
|
import { UiIcon, UiTooltip } from '@aleph-alpha/ui-library';
|
|
9
9
|
import { computed } from 'vue';
|
|
@@ -81,7 +81,7 @@ function handleClick(target: 'up' | 'down'): void {
|
|
|
81
81
|
align="center"
|
|
82
82
|
:delay-duration="0"
|
|
83
83
|
>
|
|
84
|
-
<
|
|
84
|
+
<CkMessageButton
|
|
85
85
|
v-bind="{ ariaLabel: thumbsUpLabel }"
|
|
86
86
|
:aria-pressed="isUpSelected"
|
|
87
87
|
:aria-disabled="locked || undefined"
|
|
@@ -98,7 +98,7 @@ function handleClick(target: 'up' | 'down'): void {
|
|
|
98
98
|
aria-hidden="true"
|
|
99
99
|
:class="isUpSelected ? 'fill-current' : undefined"
|
|
100
100
|
/>
|
|
101
|
-
</
|
|
101
|
+
</CkMessageButton>
|
|
102
102
|
</UiTooltip>
|
|
103
103
|
<UiTooltip
|
|
104
104
|
:content="thumbsDownLabel"
|
|
@@ -106,7 +106,7 @@ function handleClick(target: 'up' | 'down'): void {
|
|
|
106
106
|
align="center"
|
|
107
107
|
:delay-duration="0"
|
|
108
108
|
>
|
|
109
|
-
<
|
|
109
|
+
<CkMessageButton
|
|
110
110
|
v-bind="{ ariaLabel: thumbsDownLabel }"
|
|
111
111
|
:aria-pressed="isDownSelected"
|
|
112
112
|
:aria-disabled="locked || undefined"
|
|
@@ -123,7 +123,7 @@ function handleClick(target: 'up' | 'down'): void {
|
|
|
123
123
|
aria-hidden="true"
|
|
124
124
|
:class="isDownSelected ? 'fill-current' : undefined"
|
|
125
125
|
/>
|
|
126
|
-
</
|
|
126
|
+
</CkMessageButton>
|
|
127
127
|
</UiTooltip>
|
|
128
128
|
</div>
|
|
129
129
|
</template>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @category Feedback
|
|
11
11
|
* @useCases like or dislike an assistant reply, collect binary message rating, lock feedback after submission, rate AI response
|
|
12
12
|
* @keywords feedback, thumbs up, thumbs down, like, dislike, rating, vote, action, message, button
|
|
13
|
-
* @related
|
|
13
|
+
* @related CkMessageButton, CkCopyMessageButton, CkAgentMessage, CkConversation
|
|
14
14
|
*/
|
|
15
15
|
export type CkFeedbackValue = 'up' | 'down' | null;
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* capture a free-text comment, drive a two-step rating flow
|
|
19
19
|
* @keywords feedback, dialog, modal, reason codes, free text, qualitative,
|
|
20
20
|
* rating, comment
|
|
21
|
-
* @related CkFeedbackButton,
|
|
21
|
+
* @related CkFeedbackButton, CkMessageButton, CkAgentMessage,
|
|
22
22
|
* CkConversation, UiDialog, UiToggleGroup
|
|
23
23
|
*/
|
|
24
24
|
export type CkFeedbackSentiment = 'positive' | 'negative';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CkMessageButton from './CkMessageButton.vue';
|
|
2
2
|
import { render, screen } from '@testing-library/vue';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
|
-
describe('
|
|
5
|
+
describe('CkMessageButton', () => {
|
|
6
6
|
it('renders slot content inside the button', () => {
|
|
7
|
-
render(
|
|
7
|
+
render(CkMessageButton, {
|
|
8
8
|
props: { ariaLabel: 'Action' },
|
|
9
9
|
slots: { default: '<svg data-testid="icon"></svg>' },
|
|
10
10
|
});
|
|
@@ -13,7 +13,7 @@ describe('CkMessageActionButton', () => {
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
it('sets the accessible label on the button', () => {
|
|
16
|
-
render(
|
|
16
|
+
render(CkMessageButton, {
|
|
17
17
|
props: { ariaLabel: 'Copy message' },
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -23,7 +23,7 @@ describe('CkMessageActionButton', () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it('passes disabled state to the button', () => {
|
|
26
|
-
render(
|
|
26
|
+
render(CkMessageButton, {
|
|
27
27
|
props: { ariaLabel: 'Action', disabled: true },
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ describe('CkMessageActionButton', () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it('defaults to enabled', () => {
|
|
34
|
-
render(
|
|
34
|
+
render(CkMessageButton, {
|
|
35
35
|
props: { ariaLabel: 'Action' },
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CkMessageButton from './CkMessageButton.vue';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
3
3
|
import { UiIcon } from '@aleph-alpha/ui-library';
|
|
4
4
|
|
|
5
|
-
const meta: Meta<typeof
|
|
6
|
-
title: 'Base/
|
|
7
|
-
component:
|
|
5
|
+
const meta: Meta<typeof CkMessageButton> = {
|
|
6
|
+
title: 'Base/CkMessageButton',
|
|
7
|
+
component: CkMessageButton,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
10
10
|
ariaLabel: {
|
|
@@ -29,14 +29,14 @@ export default meta;
|
|
|
29
29
|
type Story = StoryObj<typeof meta>;
|
|
30
30
|
|
|
31
31
|
const defaultTemplateSource = `<script setup lang="ts">
|
|
32
|
-
import {
|
|
32
|
+
import { CkMessageButton } from '@aleph-alpha/chat-kit';
|
|
33
33
|
import { UiIcon } from '@aleph-alpha/ui-library';
|
|
34
34
|
</script>
|
|
35
35
|
|
|
36
36
|
<template>
|
|
37
|
-
<
|
|
37
|
+
<CkMessageButton ariaLabel="Copy message">
|
|
38
38
|
<UiIcon name="copy" :size="16" aria-hidden="true" />
|
|
39
|
-
</
|
|
39
|
+
</CkMessageButton>
|
|
40
40
|
</template>`;
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -45,11 +45,11 @@ import { UiIcon } from '@aleph-alpha/ui-library';
|
|
|
45
45
|
*/
|
|
46
46
|
export const Default: Story = {
|
|
47
47
|
render: () => ({
|
|
48
|
-
components: {
|
|
48
|
+
components: { CkMessageButton, UiIcon },
|
|
49
49
|
template: `
|
|
50
|
-
<
|
|
50
|
+
<CkMessageButton ariaLabel="Copy message">
|
|
51
51
|
<UiIcon name="copy" :size="16" aria-hidden="true" />
|
|
52
|
-
</
|
|
52
|
+
</CkMessageButton>
|
|
53
53
|
`,
|
|
54
54
|
}),
|
|
55
55
|
parameters: { docs: { source: { code: defaultTemplateSource } } },
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { CkMessageButtonProps } from './types';
|
|
3
3
|
import { UiButton } from '@aleph-alpha/ui-library';
|
|
4
4
|
|
|
5
5
|
defineOptions({
|
|
6
|
-
name: '
|
|
6
|
+
name: 'CkMessageButton',
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
withDefaults(defineProps<
|
|
9
|
+
withDefaults(defineProps<CkMessageButtonProps>(), {
|
|
10
10
|
disabled: false,
|
|
11
11
|
});
|
|
12
12
|
</script>
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* @category Actions
|
|
6
6
|
* @useCases message footer action, like/dislike reaction, share button on reply, secondary message action
|
|
7
7
|
* @keywords icon button, action, message, ghost, accessible, aria-label
|
|
8
|
-
* @related
|
|
8
|
+
* @related CkCopyMessageButton, CkAgentMessage, CkConversation
|
|
9
9
|
*/
|
|
10
|
-
export interface
|
|
10
|
+
export interface CkMessageButtonProps {
|
|
11
11
|
/**
|
|
12
12
|
* Accessible label for the button. **Required** because the button only
|
|
13
13
|
* renders an icon.
|