@aleph-alpha/chat-kit 5.5.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-C1busuyf.js → CkHistory.vue_vue_type_script_setup_true_lang-YHwE2DYx.js} +27 -9
  2. package/dist/adapters/responses-api.d.ts +3 -1
  3. package/dist/adapters/responses-api.d.ts.map +1 -1
  4. package/dist/adapters/responses-api.js +158 -3
  5. package/dist/chat-kit.css +1 -0
  6. package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts +6 -0
  7. package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts.map +1 -1
  8. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +6 -0
  9. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
  10. package/dist/components/index.js +1 -1
  11. package/dist/composables/index.js +4 -4
  12. package/dist/handlers/index.js +4 -4
  13. package/dist/handlers/useMessageHandler.d.ts +7 -2
  14. package/dist/handlers/useMessageHandler.d.ts.map +1 -1
  15. package/dist/helpers/conversationTree.d.ts +44 -4
  16. package/dist/helpers/conversationTree.d.ts.map +1 -1
  17. package/dist/{index-CyrVnbS9.js → index-Dn4ctxk8.js} +7 -6
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +11 -11
  21. package/dist/services/types.d.ts +7 -0
  22. package/dist/services/types.d.ts.map +1 -1
  23. package/dist/types.d.ts +38 -0
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/{useChatKitLabels-DLy0BFx8.js → useChatKitLabels-CMOr3Kjq.js} +2 -2
  26. package/dist/{useMessageHandler-N50Yj-NU.js → useMessageHandler-Tellov3w.js} +112 -30
  27. package/package.json +4 -2
  28. package/src/adapters/arena.spec.ts +86 -0
  29. package/src/adapters/responses-api.ts +276 -2
  30. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +28 -2
  31. package/src/components/composed/CkConversation/CkConversation.vue +10 -0
  32. package/src/composables/useConversations.spec.ts +2 -0
  33. package/src/composables/useConversations.ts +1 -0
  34. package/src/handlers/useMessageHandler.ts +46 -6
  35. package/src/helpers/comparison.spec.ts +114 -0
  36. package/src/helpers/conversationTree.spec.ts +98 -0
  37. package/src/helpers/conversationTree.ts +211 -26
  38. package/src/index.ts +10 -0
  39. package/src/services/types.ts +7 -0
  40. package/src/types.ts +40 -0
  41. package/src/uno-virtual.d.ts +2 -0
@@ -1,12 +1,87 @@
1
- import type { BaseMessage, ConversationTree, TreeMessage } from '../types';
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
- const base: BaseMessage = {
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
- export function addNode(tree: ConversationTree, node: TreeMessage): void {
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
- parent.activeChildIndex = parent.childrenIds.length - 1;
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(tree: ConversationTree, id: string): void {
278
+ export function cutBranchAfter(
279
+ tree: ConversationTree,
280
+ id: string,
281
+ keepChildId?: string,
282
+ ): void {
132
283
  const node = tree.nodes[id];
133
- if (!node) return;
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
- deleteSubtrees(tree, node.childrenIds);
289
+ const kept =
290
+ keepChildId !== undefined && node.childrenIds.includes(keepChildId)
291
+ ? keepChildId
292
+ : null;
136
293
 
137
- node.childrenIds = [];
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. Removing the root clears the tree.
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
@@ -1,3 +1,11 @@
1
+ // Ship only chat-kit's own utility classes (the `default` layer) in the emitted
2
+ // stylesheet. Consumers import `@aleph-alpha/chat-kit/style.css` and get every
3
+ // class the components use, without adding chat-kit to their UnoCSS content
4
+ // scan. The design-token `:root` variables live in the `preflights` layer and
5
+ // are deliberately excluded — ui-library (a peer dependency) already emits them
6
+ // in every consuming app, so shipping them here would just duplicate that layer.
7
+ import 'uno:default.css';
8
+
1
9
  // Services
2
10
  export {
3
11
  type ChatConfig,
@@ -138,7 +146,9 @@ export { formatRelativeTime } from './helpers';
138
146
 
139
147
  // Shared Types
140
148
  export type {
149
+ ActiveComparison,
141
150
  BaseMessage,
151
+ ComparisonLane,
142
152
  Conversation,
143
153
  ConversationTree,
144
154
  MessageContent,
@@ -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
  *
@@ -0,0 +1,2 @@
1
+ declare module 'uno.css';
2
+ declare module 'uno:*.css';