@aleph-alpha/chat-kit 1.1.0 → 1.2.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.
@@ -1,5 +1,6 @@
1
- import { g as generateId } from "./generateId-BHf0W4Rq.js";
1
+ import { g as generateId, u as useMessageHandler } from "./useMessageHandler-BmBdu2EW.js";
2
2
  import { ref, shallowRef, computed, readonly } from "vue";
3
+ import "./purify.es-md74IFlY.js";
3
4
  function secondsToDate(seconds) {
4
5
  return new Date(seconds * 1e3);
5
6
  }
@@ -189,7 +190,54 @@ function makePlaceholder(title) {
189
190
  updatedAt: now
190
191
  };
191
192
  }
193
+ function useConversationSessions() {
194
+ const sessions = /* @__PURE__ */ new Map();
195
+ const focusedId = ref(null);
196
+ function createSession(conversationId) {
197
+ const existing = sessions.get(conversationId);
198
+ if (existing) return existing;
199
+ const session = {
200
+ id: conversationId,
201
+ messageHandler: useMessageHandler()
202
+ };
203
+ sessions.set(conversationId, session);
204
+ return session;
205
+ }
206
+ function getSession(conversationId) {
207
+ return sessions.get(conversationId);
208
+ }
209
+ function setActiveConversation(conversationId) {
210
+ if (conversationId === null) {
211
+ focusedId.value = null;
212
+ return;
213
+ }
214
+ createSession(conversationId);
215
+ focusedId.value = conversationId;
216
+ }
217
+ function removeSession(conversationId) {
218
+ sessions.delete(conversationId);
219
+ if (focusedId.value === conversationId) {
220
+ focusedId.value = null;
221
+ }
222
+ }
223
+ const activeSession = computed(() => {
224
+ const id = focusedId.value;
225
+ return id !== null ? sessions.get(id) ?? null : null;
226
+ });
227
+ const messageHandler = computed(
228
+ () => activeSession.value?.messageHandler ?? null
229
+ );
230
+ return {
231
+ activeSession,
232
+ messageHandler,
233
+ createSession,
234
+ getSession,
235
+ setActiveConversation,
236
+ removeSession
237
+ };
238
+ }
192
239
  export {
193
- useConversations as a,
240
+ useConversationSessions as a,
241
+ useConversations as b,
194
242
  useChat as u
195
243
  };
@@ -1,4 +1,3 @@
1
- import { g as generateId } from "./generateId-BHf0W4Rq.js";
2
1
  import { reactive, ref, computed, readonly } from "vue";
3
2
  function createEmptyTree() {
4
3
  return { nodes: {}, rootId: null };
@@ -78,6 +77,9 @@ function navigate(tree, targetId) {
78
77
  if (idx === -1) return;
79
78
  parent.activeChildIndex = idx;
80
79
  }
80
+ function generateId() {
81
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : String(Date.now());
82
+ }
81
83
  const useMessageHandler = () => {
82
84
  const tree = reactive(createEmptyTree());
83
85
  const currentUserMessage = ref("");
@@ -178,5 +180,6 @@ const useMessageHandler = () => {
178
180
  };
179
181
  };
180
182
  export {
183
+ generateId as g,
181
184
  useMessageHandler as u
182
185
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aleph-alpha/chat-kit",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Chat kit package for building chat interfaces with Vue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -6,4 +6,10 @@ export type {
6
6
  UseConversationsOptions,
7
7
  UseConversationsReturn,
8
8
  } from './useConversations';
9
+ export { useConversationSessions } from './useConversationSessions';
10
+ export type {
11
+ ConversationSession,
12
+ MessageHandler,
13
+ UseConversationSessionsReturn,
14
+ } from './useConversationSessions';
9
15
  export type { ChatActions, ChatState, UseChatReturn } from './types';
@@ -0,0 +1,86 @@
1
+ import { nextTick } from 'vue';
2
+ import { useConversationSessions } from './useConversationSessions';
3
+
4
+ describe('useConversationSessions', () => {
5
+ it('starts with no active session', () => {
6
+ const sessions = useConversationSessions();
7
+
8
+ expect(sessions.activeSession.value).toBeNull();
9
+ expect(sessions.messageHandler.value).toBeNull();
10
+ expect(sessions.messageHandler.value?.messages.value ?? []).toEqual([]);
11
+ });
12
+
13
+ it('createSession is idempotent', () => {
14
+ const sessions = useConversationSessions();
15
+ const a = sessions.createSession('c1');
16
+ const b = sessions.createSession('c1');
17
+
18
+ expect(a).toBe(b);
19
+ expect(sessions.getSession('c1')).toBe(a);
20
+ });
21
+
22
+ it('isolates message trees per session', () => {
23
+ const sessions = useConversationSessions();
24
+ const sa = sessions.createSession('a');
25
+ const sb = sessions.createSession('b');
26
+
27
+ expect(sa.messageHandler).not.toBe(sb.messageHandler);
28
+
29
+ sa.messageHandler.setCurrentUserMessage('from A');
30
+ sa.messageHandler.commitUserMessage();
31
+
32
+ expect(sa.messageHandler.messages.value).toHaveLength(1);
33
+ expect(sb.messageHandler.messages.value).toHaveLength(0);
34
+ });
35
+
36
+ it('activeSession tracks the focused conversation handler', async () => {
37
+ const sessions = useConversationSessions();
38
+ sessions.createSession('a');
39
+ sessions.createSession('b');
40
+ sessions.setActiveConversation('a');
41
+
42
+ sessions.activeSession.value?.messageHandler.setCurrentUserMessage('hi');
43
+ sessions.activeSession.value?.messageHandler.commitUserMessage();
44
+
45
+ expect(
46
+ sessions.activeSession.value?.messageHandler.messages.value,
47
+ ).toHaveLength(1);
48
+ expect(
49
+ sessions.getSession('b')?.messageHandler.messages.value,
50
+ ).toHaveLength(0);
51
+
52
+ sessions.setActiveConversation('b');
53
+ await nextTick();
54
+
55
+ expect(sessions.messageHandler.value).toBe(
56
+ sessions.getSession('b')?.messageHandler,
57
+ );
58
+ expect(sessions.messageHandler.value?.messages.value).toHaveLength(0);
59
+ });
60
+
61
+ it('updating a background session does not change the focused handler messages', async () => {
62
+ const sessions = useConversationSessions();
63
+ const sa = sessions.createSession('a');
64
+ sessions.createSession('b');
65
+ sessions.setActiveConversation('b');
66
+
67
+ sa.messageHandler.setCurrentUserMessage('background');
68
+ sa.messageHandler.commitUserMessage();
69
+
70
+ await nextTick();
71
+ expect(sessions.messageHandler.value?.messages.value ?? []).toEqual([]);
72
+ expect(sa.messageHandler.messages.value).toHaveLength(1);
73
+ });
74
+
75
+ it('removeSession clears focus when that session was active', () => {
76
+ const sessions = useConversationSessions();
77
+ sessions.createSession('x');
78
+ sessions.setActiveConversation('x');
79
+
80
+ sessions.removeSession('x');
81
+
82
+ expect(sessions.getSession('x')).toBeUndefined();
83
+ expect(sessions.activeSession.value).toBeNull();
84
+ expect(sessions.messageHandler.value).toBeNull();
85
+ });
86
+ });
@@ -0,0 +1,85 @@
1
+ import { useMessageHandler } from '../handlers';
2
+ import { computed, ref, type ComputedRef, type Ref } from 'vue';
3
+
4
+ export type MessageHandler = ReturnType<typeof useMessageHandler>;
5
+
6
+ /** One conversation’s runtime message tree (`useMessageHandler` per id). */
7
+ export interface ConversationSession {
8
+ readonly id: string;
9
+ readonly messageHandler: MessageHandler;
10
+ }
11
+
12
+ /**
13
+ * Per-conversation `useMessageHandler` instances; `setActiveConversation` picks
14
+ * which session is exposed as `activeSession` / `messageHandler`.
15
+ */
16
+ export interface UseConversationSessionsReturn {
17
+ /** Focused session, or `null`. */
18
+ activeSession: ComputedRef<ConversationSession | null>;
19
+ /** `activeSession?.messageHandler`, or `null`. */
20
+ messageHandler: ComputedRef<MessageHandler | null>;
21
+ /** Get or create a session for `conversationId` (idempotent). */
22
+ createSession: (conversationId: string) => ConversationSession;
23
+ /** Session if it exists; does not create. */
24
+ getSession: (conversationId: string) => ConversationSession | undefined;
25
+ /** Focus `conversationId` (creates session if needed), or `null` to unfocus. */
26
+ setActiveConversation: (conversationId: string | null) => void;
27
+ /** Remove session; clears focus if it was active. */
28
+ removeSession: (conversationId: string) => void;
29
+ }
30
+
31
+ /** Registry of sessions by id (plain `Map` so nested refs stay valid). */
32
+ export function useConversationSessions(): UseConversationSessionsReturn {
33
+ const sessions = new Map<string, ConversationSession>();
34
+ const focusedId: Ref<string | null> = ref(null);
35
+
36
+ function createSession(conversationId: string): ConversationSession {
37
+ const existing = sessions.get(conversationId);
38
+ if (existing) return existing;
39
+
40
+ const session: ConversationSession = {
41
+ id: conversationId,
42
+ messageHandler: useMessageHandler(),
43
+ };
44
+ sessions.set(conversationId, session);
45
+ return session;
46
+ }
47
+
48
+ function getSession(conversationId: string): ConversationSession | undefined {
49
+ return sessions.get(conversationId);
50
+ }
51
+
52
+ function setActiveConversation(conversationId: string | null): void {
53
+ if (conversationId === null) {
54
+ focusedId.value = null;
55
+ return;
56
+ }
57
+ createSession(conversationId);
58
+ focusedId.value = conversationId;
59
+ }
60
+
61
+ function removeSession(conversationId: string): void {
62
+ sessions.delete(conversationId);
63
+ if (focusedId.value === conversationId) {
64
+ focusedId.value = null;
65
+ }
66
+ }
67
+
68
+ const activeSession = computed<ConversationSession | null>(() => {
69
+ const id = focusedId.value;
70
+ return id !== null ? (sessions.get(id) ?? null) : null;
71
+ });
72
+
73
+ const messageHandler = computed<MessageHandler | null>(
74
+ () => activeSession.value?.messageHandler ?? null,
75
+ );
76
+
77
+ return {
78
+ activeSession,
79
+ messageHandler,
80
+ createSession,
81
+ getSession,
82
+ setActiveConversation,
83
+ removeSession,
84
+ };
85
+ }
package/src/index.ts CHANGED
@@ -24,13 +24,20 @@ export {
24
24
  export type { ChatKitLabels } from './handlers';
25
25
 
26
26
  // Composables
27
- export { useChat, useConversations } from './composables';
27
+ export {
28
+ useChat,
29
+ useConversationSessions,
30
+ useConversations,
31
+ } from './composables';
28
32
  export type {
29
33
  ChatActions,
30
34
  ChatState,
35
+ ConversationSession,
31
36
  CreateConversationInput,
37
+ MessageHandler,
32
38
  MutationOptions,
33
39
  UseChatReturn,
40
+ UseConversationSessionsReturn,
34
41
  UseConversationsOptions,
35
42
  UseConversationsReturn,
36
43
  } from './composables';
@@ -1,6 +0,0 @@
1
- function generateId() {
2
- return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : String(Date.now());
3
- }
4
- export {
5
- generateId as g
6
- };