@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.
- package/dist/{CkInput.vue_vue_type_script_setup_true_lang-744ZHsQU.js → CkInput.vue_vue_type_script_setup_true_lang-CRymGkQs.js} +24 -69
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts +3 -4
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
- package/dist/components/base/CkHistoryItem/types.d.ts +3 -2
- package/dist/components/base/CkHistoryItem/types.d.ts.map +1 -1
- package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/index.js +3 -2
- package/dist/composables/useConversationSessions.d.ts +29 -0
- package/dist/composables/useConversationSessions.d.ts.map +1 -0
- package/dist/handlers/index.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/purify.es-md74IFlY.js +1081 -0
- package/dist/{useChatKitLabels-Dvi2FmjE.js → useChatKitLabels-BmWmMyqX.js} +75 -1152
- package/dist/{useConversations-Ca242D2P.js → useConversationSessions-ulFhtpQr.js} +50 -2
- package/dist/{useMessageHandler-uLHNac39.js → useMessageHandler-BmBdu2EW.js} +4 -1
- package/package.json +5 -5
- package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +91 -0
- package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +215 -0
- package/src/components/base/CkAgentMessage/CkAgentMessage.vue +25 -0
- package/src/components/base/CkAgentMessage/index.ts +2 -0
- package/src/components/base/CkAgentMessage/types.ts +11 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +129 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +251 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.vue +206 -0
- package/src/components/base/CkConversationLayout/index.ts +2 -0
- package/src/components/base/CkConversationLayout/types.ts +18 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts +89 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts +70 -0
- package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue +110 -0
- package/src/components/base/CkCopyMessageActionButton/index.ts +2 -0
- package/src/components/base/CkCopyMessageActionButton/types.ts +43 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +42 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +230 -0
- package/src/components/base/CkHistoryItem/CkHistoryItem.vue +285 -0
- package/src/components/base/CkHistoryItem/activeIndicator.ts +15 -0
- package/src/components/base/CkHistoryItem/index.ts +2 -0
- package/src/components/base/CkHistoryItem/types.ts +111 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.spec.ts +26 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +149 -0
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +36 -0
- package/src/components/base/CkHistoryWrapper/index.ts +2 -0
- package/src/components/base/CkHistoryWrapper/types.ts +16 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +348 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +382 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +89 -0
- package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +19 -0
- package/src/components/base/CkMarkdownRenderer/index.ts +5 -0
- package/src/components/base/CkMarkdownRenderer/types.ts +53 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.spec.ts +40 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.stories.ts +56 -0
- package/src/components/base/CkMessageActionButton/CkMessageActionButton.vue +23 -0
- package/src/components/base/CkMessageActionButton/index.ts +2 -0
- package/src/components/base/CkMessageActionButton/types.ts +21 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.spec.ts +52 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +126 -0
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.vue +25 -0
- package/src/components/base/CkPromptActionButton/index.ts +2 -0
- package/src/components/base/CkPromptActionButton/types.ts +32 -0
- package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +181 -0
- package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +224 -0
- package/src/components/base/CkPromptInput/CkPromptInput.vue +104 -0
- package/src/components/base/CkPromptInput/index.ts +2 -0
- package/src/components/base/CkPromptInput/types.ts +39 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +40 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +103 -0
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +16 -0
- package/src/components/base/CkPromptInputDisclaimer/index.ts +2 -0
- package/src/components/base/CkPromptInputDisclaimer/types.ts +17 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +64 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +81 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +27 -0
- package/src/components/base/CkPromptStopButton/index.ts +2 -0
- package/src/components/base/CkPromptStopButton/types.ts +28 -0
- package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +20 -0
- package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +112 -0
- package/src/components/base/CkUserMessage/CkUserMessage.vue +19 -0
- package/src/components/base/CkUserMessage/index.ts +2 -0
- package/src/components/base/CkUserMessage/types.ts +14 -0
- package/src/components/base/icons/CkSendIcon.vue +22 -0
- package/src/components/base/icons/CkStopIcon.vue +18 -0
- package/src/components/base/icons/index.ts +2 -0
- package/src/components/base/index.ts +21 -0
- package/src/components/composed/CkConversation/CkConversation.spec.ts +439 -0
- package/src/components/composed/CkConversation/CkConversation.stories.ts +642 -0
- package/src/components/composed/CkConversation/CkConversation.vue +146 -0
- package/src/components/composed/CkConversation/index.ts +2 -0
- package/src/components/composed/CkConversation/types.ts +37 -0
- package/src/components/composed/CkHistory/CkHistory.spec.ts +327 -0
- package/src/components/composed/CkHistory/CkHistory.stories.ts +373 -0
- package/src/components/composed/CkHistory/CkHistory.vue +126 -0
- package/src/components/composed/CkHistory/index.ts +2 -0
- package/src/components/composed/CkHistory/types.ts +72 -0
- package/src/components/composed/CkInput/CkInput.spec.ts +191 -0
- package/src/components/composed/CkInput/CkInput.stories.ts +438 -0
- package/src/components/composed/CkInput/CkInput.vue +104 -0
- package/src/components/composed/CkInput/index.ts +2 -0
- package/src/components/composed/CkInput/types.ts +60 -0
- package/src/components/composed/index.ts +5 -0
- package/src/components/index.ts +34 -0
- package/src/composables/index.ts +15 -0
- package/src/composables/types.ts +36 -0
- package/src/composables/useChat.ts +18 -0
- package/src/composables/useConversationSessions.spec.ts +86 -0
- package/src/composables/useConversationSessions.ts +85 -0
- package/src/composables/useConversations.spec.ts +557 -0
- package/src/composables/useConversations.ts +321 -0
- package/src/handlers/index.ts +9 -0
- package/src/handlers/useChatKitLabels.spec.ts +290 -0
- package/src/handlers/useChatKitLabels.ts +96 -0
- package/src/handlers/useMarkdownParser.spec.ts +112 -0
- package/src/handlers/useMarkdownParser.ts +47 -0
- package/src/handlers/useMessageHandler.spec.ts +535 -0
- package/src/handlers/useMessageHandler.ts +138 -0
- package/src/helpers/conversationTree.spec.ts +423 -0
- package/src/helpers/conversationTree.ts +117 -0
- package/src/helpers/generateId.ts +5 -0
- package/src/helpers/index.ts +4 -0
- package/src/helpers/secondsToDate.ts +7 -0
- package/src/helpers/toError.ts +8 -0
- package/src/index.ts +86 -0
- package/src/services/ChatService.ts +60 -0
- package/src/services/MockChatService.ts +47 -0
- package/src/services/index.ts +12 -0
- package/src/services/types.ts +92 -0
- package/src/test-setup.ts +12 -0
- package/src/test-utils/createTextPlugin.ts +9 -0
- package/src/types.ts +47 -0
- 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,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,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
|
+
}
|