@aleph-alpha/chat-kit 1.11.1 → 1.13.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-C1Jnn1fK.js → CkInput.vue_vue_type_script_setup_true_lang-DCZTSiBj.js} +371 -135
- package/dist/adapters/responses-api.d.ts +54 -5
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +43 -9
- package/dist/chat-kit.css +19 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts +30 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +13 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackDialog/index.d.ts +3 -0
- package/dist/components/base/CkFeedbackDialog/index.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackDialog/types.d.ts +152 -0
- package/dist/components/base/CkFeedbackDialog/types.d.ts.map +1 -0
- package/dist/components/base/index.d.ts +2 -0
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +16 -15
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.js +2 -2
- package/dist/handlers/useChatKitLabels.d.ts +18 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/handlers/useMessageHandler.d.ts +6 -2
- package/dist/handlers/useMessageHandler.d.ts.map +1 -1
- package/dist/helpers/conversationTree.d.ts +10 -1
- package/dist/helpers/conversationTree.d.ts.map +1 -1
- package/dist/{index-BLq6_Lt2.js → index-Ct5hD214.js} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -18
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/{useChatKitLabels-ftC52m7f.js → useChatKitLabels-DeHxZG2a.js} +9 -0
- package/dist/{useMessageHandler-ZRC-gIOJ.js → useMessageHandler-CMdy-lm0.js} +36 -9
- package/package.json +5 -5
- package/src/adapters/responses-api.spec.ts +70 -0
- package/src/adapters/responses-api.ts +123 -12
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.spec.ts +177 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.ts +258 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +288 -0
- package/src/components/base/CkFeedbackDialog/index.ts +2 -0
- package/src/components/base/CkFeedbackDialog/types.ts +155 -0
- package/src/components/base/index.ts +8 -0
- package/src/components/index.ts +6 -0
- package/src/handlers/useChatKitLabels.spec.ts +11 -0
- package/src/handlers/useChatKitLabels.ts +29 -0
- package/src/handlers/useMessageHandler.spec.ts +214 -0
- package/src/handlers/useMessageHandler.ts +28 -3
- package/src/helpers/conversationTree.spec.ts +134 -0
- package/src/helpers/conversationTree.ts +32 -1
- package/src/index.ts +7 -0
- package/src/types.ts +15 -0
|
@@ -118,6 +118,34 @@ describe('useMessageHandler', () => {
|
|
|
118
118
|
handler.setCurrentUserMessage(' ');
|
|
119
119
|
expect(() => handler.commitUserMessage()).toThrow('No message');
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
it('attaches metadata when supplied and exposes it on the message', () => {
|
|
123
|
+
const handler = useMessageHandler();
|
|
124
|
+
handler.setCurrentUserMessage('Hello');
|
|
125
|
+
const msg = handler.commitUserMessage({
|
|
126
|
+
metadata: {
|
|
127
|
+
attachments: [
|
|
128
|
+
{ name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(msg.metadata).toEqual({
|
|
134
|
+
attachments: [
|
|
135
|
+
{ name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
expect(handler.messages.value[0].metadata).toEqual(msg.metadata);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('omits metadata on the returned message when none is supplied', () => {
|
|
142
|
+
const handler = useMessageHandler();
|
|
143
|
+
handler.setCurrentUserMessage('Hello');
|
|
144
|
+
const msg = handler.commitUserMessage();
|
|
145
|
+
|
|
146
|
+
expect('metadata' in msg).toBe(false);
|
|
147
|
+
expect('metadata' in handler.messages.value[0]).toBe(false);
|
|
148
|
+
});
|
|
121
149
|
});
|
|
122
150
|
|
|
123
151
|
describe('updateMessage', () => {
|
|
@@ -232,6 +260,61 @@ describe('useMessageHandler', () => {
|
|
|
232
260
|
expect(getText(handler.messages.value[1].content)).toBe('Regenerated');
|
|
233
261
|
});
|
|
234
262
|
|
|
263
|
+
it('preserves existing metadata when the update omits it', () => {
|
|
264
|
+
const handler = useMessageHandler();
|
|
265
|
+
handler.setCurrentUserMessage('Hello');
|
|
266
|
+
const msg = handler.commitUserMessage({
|
|
267
|
+
metadata: { attachments: [{ name: 'report.pdf' }] },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
handler.updateMessage({
|
|
271
|
+
id: msg.id,
|
|
272
|
+
content: [text('Edited')],
|
|
273
|
+
role: 'user',
|
|
274
|
+
status: 'completed',
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(handler.getMessageById(msg.id)?.metadata).toEqual({
|
|
278
|
+
attachments: [{ name: 'report.pdf' }],
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('overwrites metadata when explicitly supplied on update', () => {
|
|
283
|
+
const handler = useMessageHandler();
|
|
284
|
+
handler.setCurrentUserMessage('Hello');
|
|
285
|
+
const msg = handler.commitUserMessage({
|
|
286
|
+
metadata: { attachments: [{ name: 'old.pdf' }] },
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
handler.updateMessage({
|
|
290
|
+
id: msg.id,
|
|
291
|
+
content: [text('Hello')],
|
|
292
|
+
role: 'user',
|
|
293
|
+
status: 'completed',
|
|
294
|
+
metadata: { attachments: [{ name: 'new.pdf' }] },
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(handler.getMessageById(msg.id)?.metadata).toEqual({
|
|
298
|
+
attachments: [{ name: 'new.pdf' }],
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('upserts a new message with metadata when supplied', () => {
|
|
303
|
+
const handler = useMessageHandler();
|
|
304
|
+
handler.updateMessage({
|
|
305
|
+
id: 'u-new',
|
|
306
|
+
content: [text('Hello')],
|
|
307
|
+
role: 'user',
|
|
308
|
+
status: 'completed',
|
|
309
|
+
metadata: { attachments: [{ name: 'fresh.pdf' }] },
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const stored = handler.getMessageById('u-new');
|
|
313
|
+
expect(stored?.metadata).toEqual({
|
|
314
|
+
attachments: [{ name: 'fresh.pdf' }],
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
235
318
|
it('does not truncate when updating the last message', () => {
|
|
236
319
|
const handler = useMessageHandler();
|
|
237
320
|
handler.setTree(
|
|
@@ -551,6 +634,137 @@ describe('useMessageHandler', () => {
|
|
|
551
634
|
});
|
|
552
635
|
});
|
|
553
636
|
|
|
637
|
+
describe('cutBranchAfter', () => {
|
|
638
|
+
it('removes every message that comes after the target on the active path', () => {
|
|
639
|
+
const handler = useMessageHandler();
|
|
640
|
+
handler.setTree(
|
|
641
|
+
makeTree(
|
|
642
|
+
{
|
|
643
|
+
u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
|
|
644
|
+
a1: makeTreeMessage({
|
|
645
|
+
id: 'a1',
|
|
646
|
+
parentId: 'u1',
|
|
647
|
+
role: 'assistant',
|
|
648
|
+
content: [text('Hi')],
|
|
649
|
+
}),
|
|
650
|
+
u2: makeTreeMessage({
|
|
651
|
+
id: 'u2',
|
|
652
|
+
parentId: 'a1',
|
|
653
|
+
content: [text('Follow-up')],
|
|
654
|
+
}),
|
|
655
|
+
a2: makeTreeMessage({
|
|
656
|
+
id: 'a2',
|
|
657
|
+
parentId: 'u2',
|
|
658
|
+
role: 'assistant',
|
|
659
|
+
content: [text('Answer')],
|
|
660
|
+
}),
|
|
661
|
+
},
|
|
662
|
+
'u1',
|
|
663
|
+
),
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
handler.cutBranchAfter('u2');
|
|
667
|
+
|
|
668
|
+
expect(handler.messages.value.map((m) => m.id)).toEqual([
|
|
669
|
+
'u1',
|
|
670
|
+
'a1',
|
|
671
|
+
'u2',
|
|
672
|
+
]);
|
|
673
|
+
expect(handler.getMessageById('a2')).toBeUndefined();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('keeps the target message and makes it the new leaf', () => {
|
|
677
|
+
const handler = useMessageHandler();
|
|
678
|
+
handler.setTree(
|
|
679
|
+
makeTree(
|
|
680
|
+
{
|
|
681
|
+
u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
|
|
682
|
+
a1: makeTreeMessage({
|
|
683
|
+
id: 'a1',
|
|
684
|
+
parentId: 'u1',
|
|
685
|
+
role: 'assistant',
|
|
686
|
+
content: [text('Hi')],
|
|
687
|
+
}),
|
|
688
|
+
u2: makeTreeMessage({
|
|
689
|
+
id: 'u2',
|
|
690
|
+
parentId: 'a1',
|
|
691
|
+
content: [text('Follow-up')],
|
|
692
|
+
}),
|
|
693
|
+
},
|
|
694
|
+
'u1',
|
|
695
|
+
),
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
handler.cutBranchAfter('a1');
|
|
699
|
+
|
|
700
|
+
expect(handler.getMessageById('a1')).toBeDefined();
|
|
701
|
+
expect(handler.currentLeafId.value).toBe('a1');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('drops sibling branches that share the cut ancestor', () => {
|
|
705
|
+
const handler = useMessageHandler();
|
|
706
|
+
handler.setTree(
|
|
707
|
+
makeTree(
|
|
708
|
+
{
|
|
709
|
+
u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
|
|
710
|
+
a1: makeTreeMessage({
|
|
711
|
+
id: 'a1',
|
|
712
|
+
parentId: 'u1',
|
|
713
|
+
role: 'assistant',
|
|
714
|
+
content: [text('Hi')],
|
|
715
|
+
}),
|
|
716
|
+
a2: makeTreeMessage({
|
|
717
|
+
id: 'a2',
|
|
718
|
+
parentId: 'u1',
|
|
719
|
+
role: 'assistant',
|
|
720
|
+
content: [text('Hey')],
|
|
721
|
+
}),
|
|
722
|
+
u2: makeTreeMessage({
|
|
723
|
+
id: 'u2',
|
|
724
|
+
parentId: 'a2',
|
|
725
|
+
content: [text('Follow-up')],
|
|
726
|
+
}),
|
|
727
|
+
},
|
|
728
|
+
'u1',
|
|
729
|
+
),
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
handler.cutBranchAfter('u1');
|
|
733
|
+
|
|
734
|
+
expect(handler.getMessageById('a1')).toBeUndefined();
|
|
735
|
+
expect(handler.getMessageById('a2')).toBeUndefined();
|
|
736
|
+
expect(handler.getMessageById('u2')).toBeUndefined();
|
|
737
|
+
expect(handler.messages.value.map((m) => m.id)).toEqual(['u1']);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('is a no-op when the target has no descendants', () => {
|
|
741
|
+
const handler = useMessageHandler();
|
|
742
|
+
handler.setCurrentUserMessage('Hello');
|
|
743
|
+
const msg = handler.commitUserMessage();
|
|
744
|
+
|
|
745
|
+
handler.cutBranchAfter(msg.id);
|
|
746
|
+
|
|
747
|
+
expect(handler.messages.value).toHaveLength(1);
|
|
748
|
+
expect(handler.messages.value[0].id).toBe(msg.id);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('is a no-op for a nonexistent id', () => {
|
|
752
|
+
const handler = useMessageHandler();
|
|
753
|
+
handler.setCurrentUserMessage('Hello');
|
|
754
|
+
handler.commitUserMessage();
|
|
755
|
+
handler.updateMessage({
|
|
756
|
+
id: 'a1',
|
|
757
|
+
content: [text('Reply')],
|
|
758
|
+
role: 'assistant',
|
|
759
|
+
status: 'completed',
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
handler.cutBranchAfter('nonexistent');
|
|
763
|
+
|
|
764
|
+
expect(handler.messages.value).toHaveLength(2);
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
554
768
|
describe('currentLeafId', () => {
|
|
555
769
|
it('is null when there are no messages', () => {
|
|
556
770
|
const handler = useMessageHandler();
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
generateId,
|
|
3
3
|
addNode as treeAddNode,
|
|
4
4
|
createEmptyTree,
|
|
5
|
+
cutBranchAfter as treeCutBranchAfter,
|
|
5
6
|
getLinearPath,
|
|
6
7
|
navigate as treeNavigate,
|
|
7
8
|
updateNode,
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
BaseMessage,
|
|
12
13
|
ConversationTree,
|
|
13
14
|
MessageContent,
|
|
15
|
+
MessageMetadata,
|
|
14
16
|
MessageRole,
|
|
15
17
|
MessageStatus,
|
|
16
18
|
TreeMessage,
|
|
@@ -36,7 +38,9 @@ export const useMessageHandler = () => {
|
|
|
36
38
|
currentUserMessage.value = '';
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
function commitUserMessage(
|
|
41
|
+
function commitUserMessage(opts?: {
|
|
42
|
+
metadata?: MessageMetadata;
|
|
43
|
+
}): BaseMessage {
|
|
40
44
|
const id = generateId();
|
|
41
45
|
const text = currentUserMessage.value.trim();
|
|
42
46
|
if (!text) throw new Error('No message');
|
|
@@ -54,18 +58,21 @@ export const useMessageHandler = () => {
|
|
|
54
58
|
content,
|
|
55
59
|
status: 'completed',
|
|
56
60
|
createdAt: new Date(),
|
|
61
|
+
...(opts?.metadata !== undefined ? { metadata: opts.metadata } : {}),
|
|
57
62
|
};
|
|
58
63
|
|
|
59
64
|
treeAddNode(tree, node);
|
|
60
65
|
clearCurrentUserMessage();
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
const base: BaseMessage = {
|
|
63
68
|
id: node.id,
|
|
64
69
|
role: node.role,
|
|
65
70
|
content: node.content,
|
|
66
71
|
createdAt: node.createdAt,
|
|
67
72
|
status: node.status,
|
|
68
73
|
};
|
|
74
|
+
if (node.metadata !== undefined) base.metadata = node.metadata;
|
|
75
|
+
return base;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
function updateMessage(message: {
|
|
@@ -73,6 +80,7 @@ export const useMessageHandler = () => {
|
|
|
73
80
|
content: MessageContent[];
|
|
74
81
|
role: MessageRole;
|
|
75
82
|
status: MessageStatus;
|
|
83
|
+
metadata?: MessageMetadata;
|
|
76
84
|
}) {
|
|
77
85
|
const existing = tree.nodes[message.id];
|
|
78
86
|
|
|
@@ -81,6 +89,9 @@ export const useMessageHandler = () => {
|
|
|
81
89
|
content: message.content,
|
|
82
90
|
role: message.role,
|
|
83
91
|
status: message.status,
|
|
92
|
+
...(message.metadata !== undefined
|
|
93
|
+
? { metadata: message.metadata }
|
|
94
|
+
: {}),
|
|
84
95
|
});
|
|
85
96
|
} else {
|
|
86
97
|
const node: TreeMessage = {
|
|
@@ -92,6 +103,9 @@ export const useMessageHandler = () => {
|
|
|
92
103
|
content: message.content,
|
|
93
104
|
status: message.status,
|
|
94
105
|
createdAt: new Date(),
|
|
106
|
+
...(message.metadata !== undefined
|
|
107
|
+
? { metadata: message.metadata }
|
|
108
|
+
: {}),
|
|
95
109
|
};
|
|
96
110
|
treeAddNode(tree, node);
|
|
97
111
|
}
|
|
@@ -100,13 +114,15 @@ export const useMessageHandler = () => {
|
|
|
100
114
|
function getMessageById(id: string): BaseMessage | undefined {
|
|
101
115
|
const node = tree.nodes[id];
|
|
102
116
|
if (!node) return undefined;
|
|
103
|
-
|
|
117
|
+
const base: BaseMessage = {
|
|
104
118
|
id: node.id,
|
|
105
119
|
role: node.role,
|
|
106
120
|
content: node.content,
|
|
107
121
|
createdAt: node.createdAt,
|
|
108
122
|
status: node.status,
|
|
123
|
+
...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
|
|
109
124
|
};
|
|
125
|
+
return base;
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
function updateMessageId(oldId: string, newId: string) {
|
|
@@ -135,6 +151,14 @@ export const useMessageHandler = () => {
|
|
|
135
151
|
treeNavigate(tree, targetId);
|
|
136
152
|
}
|
|
137
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Drop every message that comes after `messageId` on every branch beneath
|
|
156
|
+
* it. The target message is kept and becomes the new leaf of its branch.
|
|
157
|
+
*/
|
|
158
|
+
function cutBranchAfter(messageId: string) {
|
|
159
|
+
treeCutBranchAfter(tree, messageId);
|
|
160
|
+
}
|
|
161
|
+
|
|
138
162
|
return {
|
|
139
163
|
messages: messages as Ref<BaseMessage[]>,
|
|
140
164
|
currentUserMessage: readonly(currentUserMessage),
|
|
@@ -148,5 +172,6 @@ export const useMessageHandler = () => {
|
|
|
148
172
|
clearMessages,
|
|
149
173
|
setTree,
|
|
150
174
|
navigate,
|
|
175
|
+
cutBranchAfter,
|
|
151
176
|
};
|
|
152
177
|
};
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
addNode,
|
|
9
9
|
createEmptyTree,
|
|
10
|
+
cutBranchAfter,
|
|
10
11
|
getLinearPath,
|
|
11
12
|
navigate,
|
|
12
13
|
updateNode,
|
|
@@ -193,6 +194,35 @@ describe('getLinearPath', () => {
|
|
|
193
194
|
expect(u1Msg.status).toBe('completed');
|
|
194
195
|
expect(u1Msg.createdAt).toEqual(new Date('2025-01-01'));
|
|
195
196
|
});
|
|
197
|
+
|
|
198
|
+
it('forwards node metadata onto the BaseMessage projection', () => {
|
|
199
|
+
const tree = createEmptyTree();
|
|
200
|
+
addNode(
|
|
201
|
+
tree,
|
|
202
|
+
makeTreeMessage({
|
|
203
|
+
id: 'u1',
|
|
204
|
+
content: [text('Hello')],
|
|
205
|
+
metadata: {
|
|
206
|
+
attachments: [
|
|
207
|
+
{ name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' },
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const path = getLinearPath(tree);
|
|
214
|
+
expect(path[0].metadata).toEqual({
|
|
215
|
+
attachments: [{ name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' }],
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('omits metadata when the node has none', () => {
|
|
220
|
+
const tree = createEmptyTree();
|
|
221
|
+
addNode(tree, makeTreeMessage({ id: 'u1', content: [text('Hello')] }));
|
|
222
|
+
|
|
223
|
+
const path = getLinearPath(tree);
|
|
224
|
+
expect('metadata' in path[0]).toBe(false);
|
|
225
|
+
});
|
|
196
226
|
});
|
|
197
227
|
|
|
198
228
|
describe('addNode', () => {
|
|
@@ -287,6 +317,38 @@ describe('updateNode', () => {
|
|
|
287
317
|
updateNode(tree, 'nonexistent', { content: [text('x')] });
|
|
288
318
|
expect(Object.keys(tree.nodes).length).toBe(nodeCount);
|
|
289
319
|
});
|
|
320
|
+
|
|
321
|
+
it('sets metadata when supplied', () => {
|
|
322
|
+
const tree = makeBranchingTree();
|
|
323
|
+
updateNode(tree, 'u1', {
|
|
324
|
+
metadata: {
|
|
325
|
+
attachments: [{ name: 'report.pdf', fileId: 'f1' }],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
expect(tree.nodes.u1.metadata).toEqual({
|
|
329
|
+
attachments: [{ name: 'report.pdf', fileId: 'f1' }],
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('clears metadata when explicitly set to undefined', () => {
|
|
334
|
+
const tree = makeBranchingTree();
|
|
335
|
+
tree.nodes.u1.metadata = {
|
|
336
|
+
attachments: [{ name: 'old.pdf' }],
|
|
337
|
+
};
|
|
338
|
+
updateNode(tree, 'u1', { metadata: undefined });
|
|
339
|
+
expect(tree.nodes.u1.metadata).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('leaves metadata untouched when the key is absent from updates', () => {
|
|
343
|
+
const tree = makeBranchingTree();
|
|
344
|
+
tree.nodes.u1.metadata = {
|
|
345
|
+
attachments: [{ name: 'keep.pdf' }],
|
|
346
|
+
};
|
|
347
|
+
updateNode(tree, 'u1', { content: [text('Updated')] });
|
|
348
|
+
expect(tree.nodes.u1.metadata).toEqual({
|
|
349
|
+
attachments: [{ name: 'keep.pdf' }],
|
|
350
|
+
});
|
|
351
|
+
});
|
|
290
352
|
});
|
|
291
353
|
|
|
292
354
|
describe('updateNodeId', () => {
|
|
@@ -385,6 +447,78 @@ describe('navigate', () => {
|
|
|
385
447
|
});
|
|
386
448
|
});
|
|
387
449
|
|
|
450
|
+
describe('cutBranchAfter', () => {
|
|
451
|
+
it('removes every descendant of the target node', () => {
|
|
452
|
+
const tree = makeBranchingTree();
|
|
453
|
+
|
|
454
|
+
cutBranchAfter(tree, 'u2');
|
|
455
|
+
|
|
456
|
+
expect(tree.nodes.a2).toBeUndefined();
|
|
457
|
+
expect(tree.nodes.a3).toBeUndefined();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('keeps the target node and clears its children', () => {
|
|
461
|
+
const tree = makeBranchingTree();
|
|
462
|
+
|
|
463
|
+
cutBranchAfter(tree, 'u2');
|
|
464
|
+
|
|
465
|
+
expect(tree.nodes.u2).toBeDefined();
|
|
466
|
+
expect(tree.nodes.u2.childrenIds).toEqual([]);
|
|
467
|
+
expect(tree.nodes.u2.activeChildIndex).toBe(0);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('makes the target node the leaf of the active path', () => {
|
|
471
|
+
const tree = makeBranchingTree();
|
|
472
|
+
|
|
473
|
+
cutBranchAfter(tree, 'u2');
|
|
474
|
+
|
|
475
|
+
const path = getLinearPath(tree);
|
|
476
|
+
expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2']);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('removes the entire subtree, including grandchildren', () => {
|
|
480
|
+
const tree = makeBranchingTree();
|
|
481
|
+
|
|
482
|
+
cutBranchAfter(tree, 'u1');
|
|
483
|
+
|
|
484
|
+
expect(Object.keys(tree.nodes)).toEqual(['u1']);
|
|
485
|
+
expect(tree.nodes.u1.childrenIds).toEqual([]);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('only cuts descendants — sibling branches are untouched', () => {
|
|
489
|
+
const tree = makeBranchingTree();
|
|
490
|
+
|
|
491
|
+
cutBranchAfter(tree, 'a1');
|
|
492
|
+
|
|
493
|
+
expect(tree.nodes.u2).toBeUndefined();
|
|
494
|
+
expect(tree.nodes.a2).toBeUndefined();
|
|
495
|
+
expect(tree.nodes.a3).toBeUndefined();
|
|
496
|
+
|
|
497
|
+
expect(tree.nodes.a1b).toBeDefined();
|
|
498
|
+
expect(tree.nodes.u3).toBeDefined();
|
|
499
|
+
expect(tree.nodes.u1.childrenIds).toEqual(['a1', 'a1b']);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('is a no-op when the node has no children', () => {
|
|
503
|
+
const tree = makeBranchingTree();
|
|
504
|
+
const nodeCount = Object.keys(tree.nodes).length;
|
|
505
|
+
|
|
506
|
+
cutBranchAfter(tree, 'a2');
|
|
507
|
+
|
|
508
|
+
expect(Object.keys(tree.nodes).length).toBe(nodeCount);
|
|
509
|
+
expect(tree.nodes.a2.childrenIds).toEqual([]);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('is a no-op for a nonexistent id', () => {
|
|
513
|
+
const tree = makeBranchingTree();
|
|
514
|
+
const before = Object.keys(tree.nodes).sort();
|
|
515
|
+
|
|
516
|
+
cutBranchAfter(tree, 'nonexistent');
|
|
517
|
+
|
|
518
|
+
expect(Object.keys(tree.nodes).sort()).toEqual(before);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
388
522
|
describe('integration: full branching scenario', () => {
|
|
389
523
|
it('simulates send → regenerate → navigate workflow', () => {
|
|
390
524
|
const tree = createEmptyTree();
|
|
@@ -25,6 +25,7 @@ export function getLinearPath(tree: ConversationTree): BaseMessage[] {
|
|
|
25
25
|
content: node.content,
|
|
26
26
|
createdAt: node.createdAt,
|
|
27
27
|
status: node.status,
|
|
28
|
+
...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
if (node.parentId !== null) {
|
|
@@ -61,7 +62,9 @@ export function addNode(tree: ConversationTree, node: TreeMessage): void {
|
|
|
61
62
|
export function updateNode(
|
|
62
63
|
tree: ConversationTree,
|
|
63
64
|
id: string,
|
|
64
|
-
updates: Partial<
|
|
65
|
+
updates: Partial<
|
|
66
|
+
Pick<TreeMessage, 'content' | 'status' | 'role' | 'metadata'>
|
|
67
|
+
>,
|
|
65
68
|
): void {
|
|
66
69
|
const node = tree.nodes[id];
|
|
67
70
|
if (!node) return;
|
|
@@ -69,6 +72,9 @@ export function updateNode(
|
|
|
69
72
|
if (updates.content !== undefined) node.content = updates.content;
|
|
70
73
|
if (updates.status !== undefined) node.status = updates.status;
|
|
71
74
|
if (updates.role !== undefined) node.role = updates.role;
|
|
75
|
+
// `metadata` is intentionally allowed through with an `undefined` value so
|
|
76
|
+
// callers can clear it; the call site checks for `'metadata' in updates`.
|
|
77
|
+
if ('metadata' in updates) node.metadata = updates.metadata;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
export function updateNodeId(
|
|
@@ -99,6 +105,31 @@ export function updateNodeId(
|
|
|
99
105
|
if (tree.rootId === oldId) tree.rootId = newId;
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Remove every descendant of the node with the given id, keeping the node
|
|
110
|
+
* itself intact. The node's `childrenIds` is cleared and `activeChildIndex`
|
|
111
|
+
* is reset to 0, which makes the node the new leaf of its branch.
|
|
112
|
+
*
|
|
113
|
+
* Useful when editing a message and discarding the conversation that was
|
|
114
|
+
* generated after it.
|
|
115
|
+
*/
|
|
116
|
+
export function cutBranchAfter(tree: ConversationTree, id: string): void {
|
|
117
|
+
const node = tree.nodes[id];
|
|
118
|
+
if (!node) return;
|
|
119
|
+
|
|
120
|
+
const stack: string[] = [...node.childrenIds];
|
|
121
|
+
while (stack.length > 0) {
|
|
122
|
+
const currentId = stack.pop() as string;
|
|
123
|
+
const current = tree.nodes[currentId];
|
|
124
|
+
if (!current) continue;
|
|
125
|
+
stack.push(...current.childrenIds);
|
|
126
|
+
delete tree.nodes[currentId];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
node.childrenIds = [];
|
|
130
|
+
node.activeChildIndex = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
102
133
|
/**
|
|
103
134
|
* Navigate to a different branch by updating the parent's activeChildIndex
|
|
104
135
|
* to point at the target sibling.
|
package/src/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ export {
|
|
|
67
67
|
CkConversationLayout,
|
|
68
68
|
CkCopyMessageActionButton,
|
|
69
69
|
CkFeedbackButton,
|
|
70
|
+
CkFeedbackDialog,
|
|
70
71
|
CkFileItemsList,
|
|
71
72
|
CkHistory,
|
|
72
73
|
CkHistoryItem,
|
|
@@ -86,6 +87,11 @@ export type {
|
|
|
86
87
|
CkConversationLayoutProps,
|
|
87
88
|
CkConversationProps,
|
|
88
89
|
CkFeedbackButtonProps,
|
|
90
|
+
CkFeedbackDialogEmits,
|
|
91
|
+
CkFeedbackDialogProps,
|
|
92
|
+
CkFeedbackDialogSubmitPayload,
|
|
93
|
+
CkFeedbackReason,
|
|
94
|
+
CkFeedbackSentiment,
|
|
89
95
|
CkFeedbackValue,
|
|
90
96
|
CkFileEntry,
|
|
91
97
|
CkFileItemsListEmits,
|
|
@@ -107,6 +113,7 @@ export type {
|
|
|
107
113
|
ConversationTree,
|
|
108
114
|
MessageContent,
|
|
109
115
|
MessageContentStatus,
|
|
116
|
+
MessageMetadata,
|
|
110
117
|
MessageReasoningContent,
|
|
111
118
|
MessageRole,
|
|
112
119
|
MessageStatus,
|
package/src/types.ts
CHANGED
|
@@ -68,6 +68,19 @@ export type MessageContent =
|
|
|
68
68
|
| MessageToolContent
|
|
69
69
|
| MessageReasoningContent;
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Open-ended metadata bag carried alongside a message. chat-kit is
|
|
73
|
+
* intentionally agnostic about its contents — adapter authors
|
|
74
|
+
* (`responsesToTree`, …) and message handlers preserve the value verbatim
|
|
75
|
+
* through the tree → linear path projection, but never inspect it.
|
|
76
|
+
*
|
|
77
|
+
* Consumers attach their own well-known keys (attachments, citations,
|
|
78
|
+
* ratings, …) and define their own typed projections at the read site
|
|
79
|
+
* rather than extending this base type. That keeps the handler API stable
|
|
80
|
+
* even as the shape of what gets stored on a message evolves.
|
|
81
|
+
*/
|
|
82
|
+
export type MessageMetadata = Record<string, unknown>;
|
|
83
|
+
|
|
71
84
|
export interface BaseMessage {
|
|
72
85
|
id: string;
|
|
73
86
|
role: MessageRole;
|
|
@@ -76,6 +89,7 @@ export interface BaseMessage {
|
|
|
76
89
|
status: MessageStatus;
|
|
77
90
|
siblingIds?: string[];
|
|
78
91
|
siblingIndex?: number;
|
|
92
|
+
metadata?: MessageMetadata;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
export interface TreeMessage {
|
|
@@ -87,6 +101,7 @@ export interface TreeMessage {
|
|
|
87
101
|
content: MessageContent[];
|
|
88
102
|
status: MessageStatus;
|
|
89
103
|
createdAt: Date;
|
|
104
|
+
metadata?: MessageMetadata;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
export interface ConversationTree {
|