@aleph-alpha/chat-kit 5.5.0 → 5.6.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/{CkHistory.vue_vue_type_script_setup_true_lang-C1busuyf.js → CkHistory.vue_vue_type_script_setup_true_lang-DHzZgEn-.js} +26 -8
- package/dist/adapters/responses-api.d.ts +3 -1
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +158 -3
- package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts +6 -0
- package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +6 -0
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useMessageHandler.d.ts +7 -2
- package/dist/handlers/useMessageHandler.d.ts.map +1 -1
- package/dist/helpers/conversationTree.d.ts +44 -4
- package/dist/helpers/conversationTree.d.ts.map +1 -1
- package/dist/{index-CyrVnbS9.js → index-Hcwv9Hby.js} +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/services/types.d.ts +7 -0
- package/dist/services/types.d.ts.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/{useMessageHandler-N50Yj-NU.js → useMessageHandler-Tellov3w.js} +112 -30
- package/package.json +2 -2
- package/src/adapters/arena.spec.ts +86 -0
- package/src/adapters/responses-api.ts +276 -2
- package/src/components/base/CkConversationLayout/CkConversationLayout.vue +28 -2
- package/src/components/composed/CkConversation/CkConversation.vue +10 -0
- package/src/composables/useConversations.spec.ts +2 -0
- package/src/composables/useConversations.ts +1 -0
- package/src/handlers/useMessageHandler.ts +46 -6
- package/src/helpers/comparison.spec.ts +114 -0
- package/src/helpers/conversationTree.spec.ts +98 -0
- package/src/helpers/conversationTree.ts +211 -26
- package/src/index.ts +2 -0
- package/src/services/types.ts +7 -0
- package/src/types.ts +40 -0
|
@@ -518,6 +518,67 @@ describe('cutBranchAfter', () => {
|
|
|
518
518
|
|
|
519
519
|
expect(Object.keys(tree.nodes).sort()).toEqual(before);
|
|
520
520
|
});
|
|
521
|
+
|
|
522
|
+
it('clears comparison state on the target — no children left to compare', () => {
|
|
523
|
+
const tree = makeBranchingTree();
|
|
524
|
+
tree.nodes.u2.comparing = true;
|
|
525
|
+
tree.nodes.u2.comparisonWinnerId = 'a2';
|
|
526
|
+
|
|
527
|
+
cutBranchAfter(tree, 'u2');
|
|
528
|
+
|
|
529
|
+
expect(tree.nodes.u2.comparing).toBeUndefined();
|
|
530
|
+
expect(tree.nodes.u2.comparisonWinnerId).toBeUndefined();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('spares the exempted child and makes it the sole active child', () => {
|
|
534
|
+
const tree = makeBranchingTree();
|
|
535
|
+
// u2 has children [a2, a3]; keep a3.
|
|
536
|
+
cutBranchAfter(tree, 'u2', 'a3');
|
|
537
|
+
|
|
538
|
+
expect(tree.nodes.a2).toBeUndefined();
|
|
539
|
+
expect(tree.nodes.a3).toBeDefined();
|
|
540
|
+
expect(tree.nodes.u2.childrenIds).toEqual(['a3']);
|
|
541
|
+
expect(tree.nodes.u2.activeChildIndex).toBe(0);
|
|
542
|
+
expect(getLinearPath(tree).map((m) => m.id)).toEqual([
|
|
543
|
+
'u1',
|
|
544
|
+
'a1',
|
|
545
|
+
'u2',
|
|
546
|
+
'a3',
|
|
547
|
+
]);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('keeps the exempted child subtree intact', () => {
|
|
551
|
+
const tree = makeBranchingTree();
|
|
552
|
+
// Give a3 a follow-up so we can confirm the whole branch survives.
|
|
553
|
+
addNode(tree, makeTreeMessage({ id: 'u4', parentId: 'a3', role: 'user' }));
|
|
554
|
+
|
|
555
|
+
cutBranchAfter(tree, 'u2', 'a3');
|
|
556
|
+
|
|
557
|
+
expect(tree.nodes.a3).toBeDefined();
|
|
558
|
+
expect(tree.nodes.u4).toBeDefined();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('clears comparison state even when a child is spared (single lane left)', () => {
|
|
562
|
+
const tree = makeBranchingTree();
|
|
563
|
+
tree.nodes.u2.comparing = true;
|
|
564
|
+
tree.nodes.u2.comparisonWinnerId = 'a3';
|
|
565
|
+
|
|
566
|
+
cutBranchAfter(tree, 'u2', 'a3');
|
|
567
|
+
|
|
568
|
+
expect(tree.nodes.u2.childrenIds).toEqual(['a3']);
|
|
569
|
+
expect(tree.nodes.u2.comparing).toBeUndefined();
|
|
570
|
+
expect(tree.nodes.u2.comparisonWinnerId).toBeUndefined();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('ignores an unknown or non-child keepChildId and removes every branch', () => {
|
|
574
|
+
const tree = makeBranchingTree();
|
|
575
|
+
|
|
576
|
+
cutBranchAfter(tree, 'u2', 'nonexistent');
|
|
577
|
+
|
|
578
|
+
expect(tree.nodes.a2).toBeUndefined();
|
|
579
|
+
expect(tree.nodes.a3).toBeUndefined();
|
|
580
|
+
expect(tree.nodes.u2.childrenIds).toEqual([]);
|
|
581
|
+
});
|
|
521
582
|
});
|
|
522
583
|
|
|
523
584
|
describe('cutBranchAt', () => {
|
|
@@ -637,6 +698,43 @@ describe('cutBranchAt', () => {
|
|
|
637
698
|
|
|
638
699
|
expect(Object.keys(tree.nodes).sort()).toEqual(before);
|
|
639
700
|
});
|
|
701
|
+
|
|
702
|
+
it('clears the parent comparison when fewer than two lanes remain', () => {
|
|
703
|
+
const tree = makeThreeSiblingTree();
|
|
704
|
+
tree.nodes.p.comparing = true;
|
|
705
|
+
// Down to [cA] after removing cB and cC: nothing left to compare.
|
|
706
|
+
cutBranchAt(tree, 'cB');
|
|
707
|
+
cutBranchAt(tree, 'cC');
|
|
708
|
+
|
|
709
|
+
expect(tree.nodes.p.childrenIds).toEqual(['cA']);
|
|
710
|
+
expect(tree.nodes.p.comparing).toBeUndefined();
|
|
711
|
+
expect(tree.nodes.p.comparisonWinnerId).toBeUndefined();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('drops the dangling winner when the winning lane is removed but others remain', () => {
|
|
715
|
+
const tree = makeThreeSiblingTree();
|
|
716
|
+
tree.nodes.p.comparing = true;
|
|
717
|
+
tree.nodes.p.comparisonWinnerId = 'cA';
|
|
718
|
+
|
|
719
|
+
cutBranchAt(tree, 'cA');
|
|
720
|
+
|
|
721
|
+
// Still a comparison (cB, cC remain) but the recorded vote is gone.
|
|
722
|
+
expect(tree.nodes.p.childrenIds).toEqual(['cB', 'cC']);
|
|
723
|
+
expect(tree.nodes.p.comparing).toBe(true);
|
|
724
|
+
expect(tree.nodes.p.comparisonWinnerId).toBeUndefined();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('keeps the comparison and winner when a losing lane is removed', () => {
|
|
728
|
+
const tree = makeThreeSiblingTree();
|
|
729
|
+
tree.nodes.p.comparing = true;
|
|
730
|
+
tree.nodes.p.comparisonWinnerId = 'cA';
|
|
731
|
+
|
|
732
|
+
cutBranchAt(tree, 'cB');
|
|
733
|
+
|
|
734
|
+
expect(tree.nodes.p.childrenIds).toEqual(['cA', 'cC']);
|
|
735
|
+
expect(tree.nodes.p.comparing).toBe(true);
|
|
736
|
+
expect(tree.nodes.p.comparisonWinnerId).toBe('cA');
|
|
737
|
+
});
|
|
640
738
|
});
|
|
641
739
|
|
|
642
740
|
describe('integration: full branching scenario', () => {
|
|
@@ -1,12 +1,87 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ActiveComparison,
|
|
3
|
+
BaseMessage,
|
|
4
|
+
ComparisonLane,
|
|
5
|
+
ConversationTree,
|
|
6
|
+
TreeMessage,
|
|
7
|
+
} from '../types';
|
|
2
8
|
|
|
3
9
|
export function createEmptyTree(): ConversationTree {
|
|
4
10
|
return { nodes: {}, rootId: null };
|
|
5
11
|
}
|
|
6
12
|
|
|
13
|
+
/** Project a tree node to a `BaseMessage`, enriched with sibling metadata. */
|
|
14
|
+
function toBaseMessage(
|
|
15
|
+
nodes: Record<string, TreeMessage>,
|
|
16
|
+
node: TreeMessage,
|
|
17
|
+
): BaseMessage {
|
|
18
|
+
const base: BaseMessage = {
|
|
19
|
+
id: node.id,
|
|
20
|
+
role: node.role,
|
|
21
|
+
content: node.content,
|
|
22
|
+
createdAt: node.createdAt,
|
|
23
|
+
status: node.status,
|
|
24
|
+
...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (node.parentId !== null) {
|
|
28
|
+
const parent = nodes[node.parentId];
|
|
29
|
+
if (parent && parent.childrenIds.length > 1) {
|
|
30
|
+
base.siblingIds = [...parent.childrenIds];
|
|
31
|
+
base.siblingIndex = parent.childrenIds.indexOf(node.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return base;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether a node's children should render as an **active** comparison — i.e.
|
|
40
|
+
* `comparing` is set, it has at least two lanes, and it hasn't yet collapsed to
|
|
41
|
+
* history. Unvoted forks are always active (blind); a voted fork is active only
|
|
42
|
+
* until the winner branch gains a follow-up turn (after which the comparison is
|
|
43
|
+
* history and the walk follows the winner linearly).
|
|
44
|
+
*
|
|
45
|
+
* A single lane is never a comparison: mid-stream a fork can momentarily hold
|
|
46
|
+
* one lane (the other still arriving) or end up with one (a lane produced no
|
|
47
|
+
* assistant node). Treating that as active would stop `projectActivePath` early
|
|
48
|
+
* and leave no linear leaf to send from, so we require two or more children.
|
|
49
|
+
*/
|
|
50
|
+
function isActiveComparison(
|
|
51
|
+
nodes: Record<string, TreeMessage>,
|
|
52
|
+
node: TreeMessage,
|
|
53
|
+
): boolean {
|
|
54
|
+
if (!node.comparing || node.childrenIds.length < 2) return false;
|
|
55
|
+
const winnerId = node.comparisonWinnerId;
|
|
56
|
+
if (winnerId == null) return true;
|
|
57
|
+
const winner = nodes[winnerId];
|
|
58
|
+
return !winner || winner.childrenIds.length === 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Walk one lane's active sub-path from a fork-child head downward. */
|
|
62
|
+
function laneWalk(
|
|
63
|
+
nodes: Record<string, TreeMessage>,
|
|
64
|
+
headId: string,
|
|
65
|
+
): BaseMessage[] {
|
|
66
|
+
const path: BaseMessage[] = [];
|
|
67
|
+
let current: string | null = headId;
|
|
68
|
+
while (current !== null) {
|
|
69
|
+
const node: TreeMessage | undefined = nodes[current];
|
|
70
|
+
if (!node) break;
|
|
71
|
+
path.push(toBaseMessage(nodes, node));
|
|
72
|
+
if (node.childrenIds.length === 0) break;
|
|
73
|
+
current = node.childrenIds[node.activeChildIndex];
|
|
74
|
+
}
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
|
|
7
78
|
/**
|
|
8
79
|
* Walks from rootId following activeChildIndex at each node.
|
|
9
80
|
* Enriches each node with sibling metadata for the UI.
|
|
81
|
+
*
|
|
82
|
+
* Comparison-unaware: it always descends the active child. Consumers that
|
|
83
|
+
* render comparisons should use {@link projectActivePath}, which stops the
|
|
84
|
+
* linear trunk at an active comparison fork and returns its lanes separately.
|
|
10
85
|
*/
|
|
11
86
|
export function getLinearPath(tree: ConversationTree): BaseMessage[] {
|
|
12
87
|
const { nodes, rootId } = tree;
|
|
@@ -19,24 +94,7 @@ export function getLinearPath(tree: ConversationTree): BaseMessage[] {
|
|
|
19
94
|
const node: TreeMessage | undefined = nodes[current];
|
|
20
95
|
if (!node) break;
|
|
21
96
|
|
|
22
|
-
|
|
23
|
-
id: node.id,
|
|
24
|
-
role: node.role,
|
|
25
|
-
content: node.content,
|
|
26
|
-
createdAt: node.createdAt,
|
|
27
|
-
status: node.status,
|
|
28
|
-
...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
if (node.parentId !== null) {
|
|
32
|
-
const parent = nodes[node.parentId];
|
|
33
|
-
if (parent && parent.childrenIds.length > 1) {
|
|
34
|
-
base.siblingIds = [...parent.childrenIds];
|
|
35
|
-
base.siblingIndex = parent.childrenIds.indexOf(node.id);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
path.push(base);
|
|
97
|
+
path.push(toBaseMessage(nodes, node));
|
|
40
98
|
|
|
41
99
|
if (node.childrenIds.length === 0) break;
|
|
42
100
|
current = node.childrenIds[node.activeChildIndex];
|
|
@@ -45,20 +103,99 @@ export function getLinearPath(tree: ConversationTree): BaseMessage[] {
|
|
|
45
103
|
return path;
|
|
46
104
|
}
|
|
47
105
|
|
|
48
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Single-pass projection of the active view into a linear `trunk` plus (at
|
|
108
|
+
* most) one `activeComparison`. The trunk follows `activeChildIndex` from the
|
|
109
|
+
* root and **stops at an active comparison fork** (the fork parent is the last
|
|
110
|
+
* trunk item); that fork's children become the comparison lanes. History
|
|
111
|
+
* comparisons (voted + continued) are transparent — the trunk flows through
|
|
112
|
+
* the winner as ordinary linear messages.
|
|
113
|
+
*/
|
|
114
|
+
export function projectActivePath(tree: ConversationTree): {
|
|
115
|
+
trunk: BaseMessage[];
|
|
116
|
+
activeComparison: ActiveComparison | null;
|
|
117
|
+
} {
|
|
118
|
+
const { nodes, rootId } = tree;
|
|
119
|
+
const trunk: BaseMessage[] = [];
|
|
120
|
+
if (!rootId || !nodes[rootId]) return { trunk, activeComparison: null };
|
|
121
|
+
|
|
122
|
+
let current: string | null = rootId;
|
|
123
|
+
while (current !== null) {
|
|
124
|
+
const node: TreeMessage | undefined = nodes[current];
|
|
125
|
+
if (!node) break;
|
|
126
|
+
|
|
127
|
+
trunk.push(toBaseMessage(nodes, node));
|
|
128
|
+
|
|
129
|
+
if (isActiveComparison(nodes, node)) {
|
|
130
|
+
const lanes: ComparisonLane[] = node.childrenIds.map((branchId) => ({
|
|
131
|
+
branchId,
|
|
132
|
+
path: laneWalk(nodes, branchId),
|
|
133
|
+
}));
|
|
134
|
+
const winnerBranchId = node.comparisonWinnerId ?? null;
|
|
135
|
+
return {
|
|
136
|
+
trunk,
|
|
137
|
+
activeComparison: {
|
|
138
|
+
forkParentId: node.id,
|
|
139
|
+
resolved: winnerBranchId !== null,
|
|
140
|
+
winnerBranchId,
|
|
141
|
+
lanes,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (node.childrenIds.length === 0) break;
|
|
147
|
+
current = node.childrenIds[node.activeChildIndex];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { trunk, activeComparison: null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function addNode(
|
|
154
|
+
tree: ConversationTree,
|
|
155
|
+
node: TreeMessage,
|
|
156
|
+
opts: { activate?: boolean } = { activate: true },
|
|
157
|
+
): void {
|
|
49
158
|
tree.nodes[node.id] = node;
|
|
50
159
|
|
|
51
160
|
if (node.parentId !== null) {
|
|
52
161
|
const parent = tree.nodes[node.parentId];
|
|
53
162
|
if (parent && !parent.childrenIds.includes(node.id)) {
|
|
54
163
|
parent.childrenIds.push(node.id);
|
|
55
|
-
|
|
164
|
+
// Opening a comparison adds N siblings without picking a winner, so the
|
|
165
|
+
// caller suppresses activation; ordinary appends activate the new leaf.
|
|
166
|
+
if (opts.activate)
|
|
167
|
+
parent.activeChildIndex = parent.childrenIds.length - 1;
|
|
56
168
|
}
|
|
57
169
|
} else if (tree.rootId === null) {
|
|
58
170
|
tree.rootId = node.id;
|
|
59
171
|
}
|
|
60
172
|
}
|
|
61
173
|
|
|
174
|
+
/** Mark a node's children as an (unresolved) comparison. */
|
|
175
|
+
export function markComparison(tree: ConversationTree, parentId: string): void {
|
|
176
|
+
const parent = tree.nodes[parentId];
|
|
177
|
+
if (parent) parent.comparing = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Record a comparison vote: set the winner (mirrored to `activeChildIndex`) and
|
|
182
|
+
* reveal. Leaves `comparing` set — the comparison stays a labelled, revealed
|
|
183
|
+
* view until the user continues past the winner (which collapses it to
|
|
184
|
+
* history). No-op if the parent/winner is unknown.
|
|
185
|
+
*/
|
|
186
|
+
export function voteComparison(
|
|
187
|
+
tree: ConversationTree,
|
|
188
|
+
parentId: string,
|
|
189
|
+
winnerBranchId: string,
|
|
190
|
+
): void {
|
|
191
|
+
const parent = tree.nodes[parentId];
|
|
192
|
+
if (!parent) return;
|
|
193
|
+
const idx = parent.childrenIds.indexOf(winnerBranchId);
|
|
194
|
+
if (idx === -1) return;
|
|
195
|
+
parent.comparisonWinnerId = winnerBranchId;
|
|
196
|
+
parent.activeChildIndex = idx;
|
|
197
|
+
}
|
|
198
|
+
|
|
62
199
|
export function updateNode(
|
|
63
200
|
tree: ConversationTree,
|
|
64
201
|
id: string,
|
|
@@ -125,17 +262,48 @@ function deleteSubtrees(tree: ConversationTree, ids: readonly string[]): void {
|
|
|
125
262
|
* itself intact. The node's `childrenIds` is cleared and `activeChildIndex`
|
|
126
263
|
* is reset to 0, which makes the node the new leaf of its branch.
|
|
127
264
|
*
|
|
265
|
+
* Pass `keepChildId` (a *direct* child of the node) to exempt one branch: that
|
|
266
|
+
* child and its subtree survive and become the node's sole, active child while
|
|
267
|
+
* every sibling branch is dropped. Useful when picking a comparison winner and
|
|
268
|
+
* discarding the alternatives. An unknown or non-child `keepChildId` is ignored
|
|
269
|
+
* (every branch is removed, as with no exception).
|
|
270
|
+
*
|
|
271
|
+
* Either way the node is left with at most one child, so any comparison state
|
|
272
|
+
* on it (`comparing` / `comparisonWinnerId`) — impossible with a single lane —
|
|
273
|
+
* is cleared.
|
|
274
|
+
*
|
|
128
275
|
* Useful when editing a message and discarding the conversation that was
|
|
129
276
|
* generated after it.
|
|
130
277
|
*/
|
|
131
|
-
export function cutBranchAfter(
|
|
278
|
+
export function cutBranchAfter(
|
|
279
|
+
tree: ConversationTree,
|
|
280
|
+
id: string,
|
|
281
|
+
keepChildId?: string,
|
|
282
|
+
): void {
|
|
132
283
|
const node = tree.nodes[id];
|
|
133
|
-
|
|
284
|
+
// Guard against a crafted id (`__proto__`, `constructor`) resolving to a
|
|
285
|
+
// built-in prototype object rather than a real node: the mutations below
|
|
286
|
+
// would otherwise corrupt `Object.prototype`.
|
|
287
|
+
if (!node || !Object.prototype.hasOwnProperty.call(tree.nodes, id)) return;
|
|
134
288
|
|
|
135
|
-
|
|
289
|
+
const kept =
|
|
290
|
+
keepChildId !== undefined && node.childrenIds.includes(keepChildId)
|
|
291
|
+
? keepChildId
|
|
292
|
+
: null;
|
|
136
293
|
|
|
137
|
-
|
|
294
|
+
deleteSubtrees(
|
|
295
|
+
tree,
|
|
296
|
+
kept === null
|
|
297
|
+
? node.childrenIds
|
|
298
|
+
: node.childrenIds.filter((childId) => childId !== kept),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
node.childrenIds = kept === null ? [] : [kept];
|
|
138
302
|
node.activeChildIndex = 0;
|
|
303
|
+
// With at most one child left there is nothing to compare, so any comparison
|
|
304
|
+
// state on the node is stale — drop it so the node reads as an ordinary leaf.
|
|
305
|
+
delete node.comparing;
|
|
306
|
+
delete node.comparisonWinnerId;
|
|
139
307
|
}
|
|
140
308
|
|
|
141
309
|
/**
|
|
@@ -146,7 +314,10 @@ export function cutBranchAfter(tree: ConversationTree, id: string): void {
|
|
|
146
314
|
* (e.g. an interrupted regenerate) without cropping the others.
|
|
147
315
|
*
|
|
148
316
|
* The parent's `activeChildIndex` is clamped to a remaining sibling so the
|
|
149
|
-
* active path stays valid
|
|
317
|
+
* active path stays valid, and its comparison state is reconciled: the whole
|
|
318
|
+
* comparison is dropped once fewer than two lanes survive, and a dangling
|
|
319
|
+
* `comparisonWinnerId` is cleared when the removed branch was the winner.
|
|
320
|
+
* Removing the root clears the tree.
|
|
150
321
|
*
|
|
151
322
|
* Prefer {@link cutBranchAfter} when it fits the case: unlinking here scans
|
|
152
323
|
* and splices the parent's `childrenIds` (O(siblings)), whereas
|
|
@@ -177,6 +348,20 @@ export function cutBranchAt(tree: ConversationTree, id: string): void {
|
|
|
177
348
|
if (parent.activeChildIndex >= parent.childrenIds.length) {
|
|
178
349
|
parent.activeChildIndex = Math.max(0, parent.childrenIds.length - 1);
|
|
179
350
|
}
|
|
351
|
+
|
|
352
|
+
if (parent.comparing) {
|
|
353
|
+
// A comparison needs at least two lanes; once we're down to a single (or
|
|
354
|
+
// no) remaining child there is nothing left to compare, so clear the
|
|
355
|
+
// comparison entirely and let the survivor render as ordinary history.
|
|
356
|
+
if (parent.childrenIds.length < 2) {
|
|
357
|
+
delete parent.comparing;
|
|
358
|
+
delete parent.comparisonWinnerId;
|
|
359
|
+
} else if (parent.comparisonWinnerId === id) {
|
|
360
|
+
// The removed branch was the recorded winner; drop the (now dangling)
|
|
361
|
+
// vote so the comparison falls back to the blind, awaiting-vote state.
|
|
362
|
+
delete parent.comparisonWinnerId;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
180
365
|
}
|
|
181
366
|
|
|
182
367
|
/**
|
package/src/index.ts
CHANGED
package/src/services/types.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface ConversationSummary {
|
|
|
28
28
|
title: string;
|
|
29
29
|
createdAt: Date;
|
|
30
30
|
updatedAt: Date;
|
|
31
|
+
/**
|
|
32
|
+
* The raw conversation metadata, passed through untouched — including the
|
|
33
|
+
* `title` (also surfaced above as a convenience). Lets consumers read
|
|
34
|
+
* app-specific keys — e.g. arena winners — from a `fetchById` result without
|
|
35
|
+
* a second request. `null` when the server returned no metadata.
|
|
36
|
+
*/
|
|
37
|
+
metadata?: Record<string, unknown> | null;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
/**
|
package/src/types.ts
CHANGED
|
@@ -102,6 +102,21 @@ export interface TreeMessage {
|
|
|
102
102
|
status: MessageStatus;
|
|
103
103
|
createdAt: Date;
|
|
104
104
|
metadata?: MessageMetadata;
|
|
105
|
+
/**
|
|
106
|
+
* When set, this node's `childrenIds` are a **comparison** (a preference
|
|
107
|
+
* pair / draft set rendered side-by-side) rather than ordinary regenerate
|
|
108
|
+
* alternatives. Set when the comparison is opened and left set once voted —
|
|
109
|
+
* the comparison stays a distinct, labelled view until the user continues
|
|
110
|
+
* past the winner, at which point it collapses to linear history.
|
|
111
|
+
*/
|
|
112
|
+
comparing?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* The child id chosen when a `comparing` node is voted on (mirrored to
|
|
115
|
+
* `activeChildIndex`). Absent = not yet voted. Its presence flips the
|
|
116
|
+
* comparison from the blind "awaiting vote" state to the revealed "resolved"
|
|
117
|
+
* state; once the winner branch gains children the comparison is history.
|
|
118
|
+
*/
|
|
119
|
+
comparisonWinnerId?: string;
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
export interface ConversationTree {
|
|
@@ -109,6 +124,31 @@ export interface ConversationTree {
|
|
|
109
124
|
rootId: string | null;
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
/**
|
|
128
|
+
* One side of an active comparison: the head child that defines the lane plus
|
|
129
|
+
* that lane's full active sub-path (length 1 for a fresh pair, longer once the
|
|
130
|
+
* conversation has continued inside the lane).
|
|
131
|
+
*/
|
|
132
|
+
export interface ComparisonLane {
|
|
133
|
+
branchId: string;
|
|
134
|
+
path: BaseMessage[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The single active comparison surfaced alongside the linear `messages` path.
|
|
139
|
+
* Non-null only while the active path's tail fork is unresolved (blind) or
|
|
140
|
+
* voted-but-not-continued (revealed). Its `forkParentId` is the last item of
|
|
141
|
+
* `messages`; everything below the fork lives in `lanes`.
|
|
142
|
+
*/
|
|
143
|
+
export interface ActiveComparison {
|
|
144
|
+
forkParentId: string;
|
|
145
|
+
/** false = awaiting vote (blind); true = voted, not yet continued (revealed). */
|
|
146
|
+
resolved: boolean;
|
|
147
|
+
/** The chosen lane's head id once resolved. */
|
|
148
|
+
winnerBranchId: string | null;
|
|
149
|
+
lanes: ComparisonLane[];
|
|
150
|
+
}
|
|
151
|
+
|
|
112
152
|
/**
|
|
113
153
|
* High-level conversation record consumed by `CkHistory`.
|
|
114
154
|
*
|