@aleph-alpha/chat-kit 1.0.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.
Files changed (133) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-744ZHsQU.js → CkInput.vue_vue_type_script_setup_true_lang-CRymGkQs.js} +24 -69
  2. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts +3 -4
  3. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
  4. package/dist/components/base/CkHistoryItem/types.d.ts +3 -2
  5. package/dist/components/base/CkHistoryItem/types.d.ts.map +1 -1
  6. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
  7. package/dist/components/index.js +1 -1
  8. package/dist/composables/index.d.ts +2 -0
  9. package/dist/composables/index.d.ts.map +1 -1
  10. package/dist/composables/index.js +3 -2
  11. package/dist/composables/useConversationSessions.d.ts +29 -0
  12. package/dist/composables/useConversationSessions.d.ts.map +1 -0
  13. package/dist/handlers/index.js +2 -2
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -6
  17. package/dist/purify.es-md74IFlY.js +1081 -0
  18. package/dist/{useChatKitLabels-Dvi2FmjE.js → useChatKitLabels-BmWmMyqX.js} +75 -1152
  19. package/dist/{useConversations-Ca242D2P.js → useConversationSessions-ulFhtpQr.js} +50 -2
  20. package/dist/{useMessageHandler-uLHNac39.js → useMessageHandler-BmBdu2EW.js} +4 -1
  21. package/package.json +5 -5
  22. package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +91 -0
  23. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +215 -0
  24. package/src/components/base/CkAgentMessage/CkAgentMessage.vue +25 -0
  25. package/src/components/base/CkAgentMessage/index.ts +2 -0
  26. package/src/components/base/CkAgentMessage/types.ts +11 -0
  27. package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +129 -0
  28. package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +251 -0
  29. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +206 -0
  30. package/src/components/base/CkConversationLayout/index.ts +2 -0
  31. package/src/components/base/CkConversationLayout/types.ts +18 -0
  32. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts +89 -0
  33. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts +70 -0
  34. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue +110 -0
  35. package/src/components/base/CkCopyMessageActionButton/index.ts +2 -0
  36. package/src/components/base/CkCopyMessageActionButton/types.ts +43 -0
  37. package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +42 -0
  38. package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +230 -0
  39. package/src/components/base/CkHistoryItem/CkHistoryItem.vue +285 -0
  40. package/src/components/base/CkHistoryItem/activeIndicator.ts +15 -0
  41. package/src/components/base/CkHistoryItem/index.ts +2 -0
  42. package/src/components/base/CkHistoryItem/types.ts +111 -0
  43. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.spec.ts +26 -0
  44. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +149 -0
  45. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +36 -0
  46. package/src/components/base/CkHistoryWrapper/index.ts +2 -0
  47. package/src/components/base/CkHistoryWrapper/types.ts +16 -0
  48. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +348 -0
  49. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +382 -0
  50. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +89 -0
  51. package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +19 -0
  52. package/src/components/base/CkMarkdownRenderer/index.ts +5 -0
  53. package/src/components/base/CkMarkdownRenderer/types.ts +53 -0
  54. package/src/components/base/CkMessageActionButton/CkMessageActionButton.spec.ts +40 -0
  55. package/src/components/base/CkMessageActionButton/CkMessageActionButton.stories.ts +56 -0
  56. package/src/components/base/CkMessageActionButton/CkMessageActionButton.vue +23 -0
  57. package/src/components/base/CkMessageActionButton/index.ts +2 -0
  58. package/src/components/base/CkMessageActionButton/types.ts +21 -0
  59. package/src/components/base/CkPromptActionButton/CkPromptActionButton.spec.ts +52 -0
  60. package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +126 -0
  61. package/src/components/base/CkPromptActionButton/CkPromptActionButton.vue +25 -0
  62. package/src/components/base/CkPromptActionButton/index.ts +2 -0
  63. package/src/components/base/CkPromptActionButton/types.ts +32 -0
  64. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +181 -0
  65. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +224 -0
  66. package/src/components/base/CkPromptInput/CkPromptInput.vue +104 -0
  67. package/src/components/base/CkPromptInput/index.ts +2 -0
  68. package/src/components/base/CkPromptInput/types.ts +39 -0
  69. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +40 -0
  70. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +103 -0
  71. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +16 -0
  72. package/src/components/base/CkPromptInputDisclaimer/index.ts +2 -0
  73. package/src/components/base/CkPromptInputDisclaimer/types.ts +17 -0
  74. package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +64 -0
  75. package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +81 -0
  76. package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +27 -0
  77. package/src/components/base/CkPromptStopButton/index.ts +2 -0
  78. package/src/components/base/CkPromptStopButton/types.ts +28 -0
  79. package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +20 -0
  80. package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +112 -0
  81. package/src/components/base/CkUserMessage/CkUserMessage.vue +19 -0
  82. package/src/components/base/CkUserMessage/index.ts +2 -0
  83. package/src/components/base/CkUserMessage/types.ts +14 -0
  84. package/src/components/base/icons/CkSendIcon.vue +22 -0
  85. package/src/components/base/icons/CkStopIcon.vue +18 -0
  86. package/src/components/base/icons/index.ts +2 -0
  87. package/src/components/base/index.ts +21 -0
  88. package/src/components/composed/CkConversation/CkConversation.spec.ts +439 -0
  89. package/src/components/composed/CkConversation/CkConversation.stories.ts +642 -0
  90. package/src/components/composed/CkConversation/CkConversation.vue +146 -0
  91. package/src/components/composed/CkConversation/index.ts +2 -0
  92. package/src/components/composed/CkConversation/types.ts +37 -0
  93. package/src/components/composed/CkHistory/CkHistory.spec.ts +327 -0
  94. package/src/components/composed/CkHistory/CkHistory.stories.ts +373 -0
  95. package/src/components/composed/CkHistory/CkHistory.vue +126 -0
  96. package/src/components/composed/CkHistory/index.ts +2 -0
  97. package/src/components/composed/CkHistory/types.ts +72 -0
  98. package/src/components/composed/CkInput/CkInput.spec.ts +191 -0
  99. package/src/components/composed/CkInput/CkInput.stories.ts +438 -0
  100. package/src/components/composed/CkInput/CkInput.vue +104 -0
  101. package/src/components/composed/CkInput/index.ts +2 -0
  102. package/src/components/composed/CkInput/types.ts +60 -0
  103. package/src/components/composed/index.ts +5 -0
  104. package/src/components/index.ts +34 -0
  105. package/src/composables/index.ts +15 -0
  106. package/src/composables/types.ts +36 -0
  107. package/src/composables/useChat.ts +18 -0
  108. package/src/composables/useConversationSessions.spec.ts +86 -0
  109. package/src/composables/useConversationSessions.ts +85 -0
  110. package/src/composables/useConversations.spec.ts +557 -0
  111. package/src/composables/useConversations.ts +321 -0
  112. package/src/handlers/index.ts +9 -0
  113. package/src/handlers/useChatKitLabels.spec.ts +290 -0
  114. package/src/handlers/useChatKitLabels.ts +96 -0
  115. package/src/handlers/useMarkdownParser.spec.ts +112 -0
  116. package/src/handlers/useMarkdownParser.ts +47 -0
  117. package/src/handlers/useMessageHandler.spec.ts +535 -0
  118. package/src/handlers/useMessageHandler.ts +138 -0
  119. package/src/helpers/conversationTree.spec.ts +423 -0
  120. package/src/helpers/conversationTree.ts +117 -0
  121. package/src/helpers/generateId.ts +5 -0
  122. package/src/helpers/index.ts +4 -0
  123. package/src/helpers/secondsToDate.ts +7 -0
  124. package/src/helpers/toError.ts +8 -0
  125. package/src/index.ts +86 -0
  126. package/src/services/ChatService.ts +60 -0
  127. package/src/services/MockChatService.ts +47 -0
  128. package/src/services/index.ts +12 -0
  129. package/src/services/types.ts +92 -0
  130. package/src/test-setup.ts +12 -0
  131. package/src/test-utils/createTextPlugin.ts +9 -0
  132. package/src/types.ts +47 -0
  133. package/dist/generateId-BHf0W4Rq.js +0 -6
@@ -0,0 +1,104 @@
1
+ <script setup lang="ts">
2
+ import CkPromptActionButton from '../../base/CkPromptActionButton/CkPromptActionButton.vue';
3
+ import CkPromptStopButton from '../../base/CkPromptStopButton/CkPromptStopButton.vue';
4
+ import CkPromptInput from '../../base/CkPromptInput/CkPromptInput.vue';
5
+ import CkPromptInputDisclaimer from '../../base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue';
6
+ import CkSendIcon from '../../base/icons/CkSendIcon.vue';
7
+ import CkStopIcon from '../../base/icons/CkStopIcon.vue';
8
+ import type { CkInputProps } from './types';
9
+ import { computed, useTemplateRef } from 'vue';
10
+
11
+ defineOptions({ name: 'CkInput' });
12
+
13
+ const props = withDefaults(defineProps<CkInputProps>(), {
14
+ modelValue: '',
15
+ placeholder: 'Ask me anything',
16
+ disabled: false,
17
+ loading: false,
18
+ maxHeight: 200,
19
+ ariaLabel: 'Chat message input',
20
+ stopAriaLabel: 'Stop generating',
21
+ disclaimer: undefined,
22
+ });
23
+
24
+ const emit = defineEmits<{
25
+ (e: 'update:modelValue', value: string): void;
26
+ (e: 'submit', value: string): void;
27
+ (e: 'stop'): void;
28
+ }>();
29
+
30
+ const promptInputRef =
31
+ useTemplateRef<InstanceType<typeof CkPromptInput>>('promptInputRef');
32
+
33
+ const isDisabled = computed(() => props.disabled || props.loading);
34
+
35
+ function onUpdateModelValue(value: string): void {
36
+ emit('update:modelValue', value);
37
+ }
38
+
39
+ function onSubmit(value: string): void {
40
+ emit('submit', value);
41
+ }
42
+
43
+ function onStop(): void {
44
+ emit('stop');
45
+ }
46
+
47
+ function focus(): void {
48
+ promptInputRef.value?.focus();
49
+ }
50
+
51
+ function submit(): void {
52
+ promptInputRef.value?.submit();
53
+ }
54
+
55
+ defineExpose({ focus, submit });
56
+ </script>
57
+
58
+ <template>
59
+ <CkPromptInput
60
+ ref="promptInputRef"
61
+ :model-value="modelValue"
62
+ :placeholder="placeholder"
63
+ :disabled="isDisabled"
64
+ :max-height="maxHeight"
65
+ :aria-label="ariaLabel"
66
+ @update:model-value="onUpdateModelValue"
67
+ @submit="onSubmit"
68
+ >
69
+ <template v-if="$slots['toolbar-left']" #toolbar-left>
70
+ <slot name="toolbar-left" />
71
+ </template>
72
+
73
+ <template #toolbar-right="scope">
74
+ <slot name="toolbar-right" v-bind="scope">
75
+ <CkPromptStopButton
76
+ v-if="loading"
77
+ :aria-label="stopAriaLabel"
78
+ :disabled="disabled"
79
+ @click="onStop"
80
+ >
81
+ <slot name="stop-icon">
82
+ <CkStopIcon />
83
+ </slot>
84
+ </CkPromptStopButton>
85
+ <CkPromptActionButton
86
+ v-else
87
+ v-bind="{ ariaLabel: 'Send message' }"
88
+ type="submit"
89
+ :disabled="scope.disabled"
90
+ >
91
+ <slot name="send-icon">
92
+ <CkSendIcon />
93
+ </slot>
94
+ </CkPromptActionButton>
95
+ </slot>
96
+ </template>
97
+
98
+ <template #footer>
99
+ <slot name="footer">
100
+ <CkPromptInputDisclaimer v-if="disclaimer" :text="disclaimer" />
101
+ </slot>
102
+ </template>
103
+ </CkPromptInput>
104
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkInput } from './CkInput.vue';
2
+ export type * from './types';
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Drop-in chat input: combines `CkPromptInput`, a `CkPromptActionButton` send
3
+ * button, and a `CkPromptInputDisclaimer`. Supports `v-model`, loading and
4
+ * disabled states, and slots to customize the toolbar and footer.
5
+ *
6
+ * @category Form Inputs
7
+ * @useCases chat compose input, prompt box with send button, llm input with disclaimer, quick-start chat input, turnkey chat input
8
+ * @keywords input, prompt, chat, send, compose, disclaimer, loading, v-model, textarea
9
+ * @related CkPromptInput, CkPromptActionButton, CkPromptInputDisclaimer, CkConversation
10
+ */
11
+ export interface CkInputProps {
12
+ /**
13
+ * Current value of the input. Supports `v-model`.
14
+ * @default ''
15
+ */
16
+ modelValue?: string;
17
+ /**
18
+ * Placeholder shown when the input is empty.
19
+ * @default 'Ask me anything'
20
+ */
21
+ placeholder?: string;
22
+ /**
23
+ * Whether the input is disabled. Also implied when `loading` is `true`.
24
+ * @default false
25
+ */
26
+ disabled?: boolean;
27
+ /**
28
+ * Shows a spinner in the send button and disables the input while a reply
29
+ * is being generated.
30
+ * @default false
31
+ */
32
+ loading?: boolean;
33
+ /**
34
+ * Max height (in px) of the underlying textarea before it becomes scrollable.
35
+ * @default 200
36
+ */
37
+ maxHeight?: number;
38
+ /**
39
+ * Accessible label for the input.
40
+ * @default 'Chat message input'
41
+ */
42
+ ariaLabel?: string;
43
+ /**
44
+ * Accessible label for the stop generating button.
45
+ * @default 'Stop generating'
46
+ */
47
+ stopAriaLabel?: string;
48
+ /**
49
+ * Optional disclaimer text rendered below the input via
50
+ * `CkPromptInputDisclaimer`. Omit to render no disclaimer.
51
+ */
52
+ disclaimer?: string;
53
+ }
54
+
55
+ export interface CkInputEmits {
56
+ /** Emitted on every keystroke. */
57
+ (e: 'update:modelValue', value: string): void;
58
+ /** Emitted when the user submits via Enter or the send button. */
59
+ (e: 'submit', value: string): void;
60
+ }
@@ -0,0 +1,5 @@
1
+ export { CkConversation } from './CkConversation';
2
+ export type { CkConversationProps } from './CkConversation';
3
+ export { CkHistory } from './CkHistory';
4
+ export type { CkHistoryEmits, CkHistoryProps } from './CkHistory';
5
+ export { CkInput } from './CkInput';
@@ -0,0 +1,34 @@
1
+ // Base (presentational) components
2
+ export {
3
+ CkAgentMessage,
4
+ CkConversationLayout,
5
+ CkCopyMessageActionButton,
6
+ CkHistoryItem,
7
+ CkHistoryWrapper,
8
+ CkMarkdownRenderer,
9
+ CkMessageActionButton,
10
+ CkPromptActionButton,
11
+ CkPromptStopButton,
12
+ CkPromptInput,
13
+ CkPromptInputDisclaimer,
14
+ CkSendIcon,
15
+ CkStopIcon,
16
+ CkUserMessage,
17
+ } from './base';
18
+ export type {
19
+ CkConversationLayoutProps,
20
+ CkHistoryItemEmits,
21
+ CkHistoryItemProps,
22
+ CkHistoryWrapperProps,
23
+ CkMarkdownElementClasses,
24
+ CkMarkdownRendererProps,
25
+ CkUserMessageProps,
26
+ } from './base';
27
+
28
+ // Composed (pattern) components
29
+ export { CkConversation, CkHistory, CkInput } from './composed';
30
+ export type {
31
+ CkConversationProps,
32
+ CkHistoryEmits,
33
+ CkHistoryProps,
34
+ } from './composed';
@@ -0,0 +1,15 @@
1
+ export { useChat } from './useChat';
2
+ export { useConversations } from './useConversations';
3
+ export type {
4
+ CreateConversationInput,
5
+ MutationOptions,
6
+ UseConversationsOptions,
7
+ UseConversationsReturn,
8
+ } from './useConversations';
9
+ export { useConversationSessions } from './useConversationSessions';
10
+ export type {
11
+ ConversationSession,
12
+ MessageHandler,
13
+ UseConversationSessionsReturn,
14
+ } from './useConversationSessions';
15
+ export type { ChatActions, ChatState, UseChatReturn } from './types';
@@ -0,0 +1,36 @@
1
+ import type { ChatMessage, ChatResponse } from '../services/types';
2
+ import type { BaseMessage } from '../types';
3
+ import type { Ref } from 'vue';
4
+
5
+ /**
6
+ * State managed by the chat composable
7
+ */
8
+ export interface ChatState {
9
+ messages: BaseMessage[];
10
+ isLoading: Ref<boolean>;
11
+ error: Ref<Error | null>;
12
+ }
13
+
14
+ /**
15
+ * Actions provided by the chat composable
16
+ */
17
+ export interface ChatActions {
18
+ sendMessage: (message: ChatMessage) => Promise<void>;
19
+ clearMessages: () => void;
20
+ retryLastMessage: () => Promise<void>;
21
+ }
22
+
23
+ /**
24
+ * Return type for the useChat composable
25
+ */
26
+ export interface UseChatReturn {
27
+ // State
28
+ messages: BaseMessage[];
29
+ isLoading: Ref<boolean>;
30
+ error: Ref<Error | null>;
31
+
32
+ // Actions
33
+ sendMessage: (message: ChatMessage) => Promise<ChatResponse>;
34
+ clearMessages?: () => void;
35
+ retryLastMessage?: () => Promise<void>;
36
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Composable function signature for chat functionality
3
+ *
4
+ * This is a skeleton function signature that defines the contract
5
+ * for the useChat composable. Implementations should provide the
6
+ * actual logic for managing chat state and interactions.
7
+ *
8
+ * @param service - The chat service instance to use
9
+ * @param config - Optional configuration for the composable
10
+ * @returns Object containing chat state and actions
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const chat = useChat();
15
+ * await chat.sendMessage({ role: 'user', content: 'Hello' });
16
+ * ```
17
+ */
18
+ export function useChat() {}
@@ -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
+ }