@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,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CkUserMessageProps } from './types';
|
|
3
|
+
|
|
4
|
+
defineOptions({ name: 'CkUserMessage' });
|
|
5
|
+
|
|
6
|
+
defineProps<CkUserMessageProps>();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div
|
|
11
|
+
class="bg-background-surface-primary inline-flex max-w-[600px] flex-col items-end justify-start gap-4 rounded-xl p-6"
|
|
12
|
+
>
|
|
13
|
+
<div
|
|
14
|
+
class="text-content-on-surface-primary justify-start self-stretch text-base leading-6 font-normal"
|
|
15
|
+
>
|
|
16
|
+
<slot>{{ message }}</slot>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble layout for a single user turn in a conversation. Renders the message
|
|
3
|
+
* text on the right side with a tokenized background and max-width; supports
|
|
4
|
+
* a default slot for custom inline content.
|
|
5
|
+
*
|
|
6
|
+
* @category Messages
|
|
7
|
+
* @useCases render user message bubble, conversation turn from user, chat input preview, right-aligned message
|
|
8
|
+
* @keywords user, message, bubble, chat, conversation, turn
|
|
9
|
+
* @related CkAgentMessage, CkConversation
|
|
10
|
+
*/
|
|
11
|
+
export interface CkUserMessageProps {
|
|
12
|
+
/** Text content of the user message. Rendered inside the bubble when no default slot is provided. */
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineOptions({ name: 'CkSendIcon' });
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
width="16"
|
|
9
|
+
height="16"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
stroke-width="2"
|
|
14
|
+
stroke-linecap="round"
|
|
15
|
+
stroke-linejoin="round"
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"
|
|
19
|
+
/>
|
|
20
|
+
<path d="M6 12h16" />
|
|
21
|
+
</svg>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineOptions({ name: 'CkStopIcon' });
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
width="16"
|
|
9
|
+
height="16"
|
|
10
|
+
viewBox="0 0 16 16"
|
|
11
|
+
fill="none"
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
d="M13.3335 3.33301C13.3335 2.96482 13.035 2.66634 12.6668 2.66634H3.3335C2.96531 2.66634 2.66683 2.96482 2.66683 3.33301V12.6663C2.66683 13.0345 2.96531 13.333 3.3335 13.333H12.6668C13.035 13.333 13.3335 13.0345 13.3335 12.6663V3.33301ZM14.6668 12.6663C14.6668 13.7709 13.7714 14.6663 12.6668 14.6663H3.3335C2.22893 14.6663 1.3335 13.7709 1.3335 12.6663V3.33301C1.3335 2.22844 2.22893 1.33301 3.3335 1.33301H12.6668C13.7714 1.33301 14.6668 2.22844 14.6668 3.33301V12.6663Z"
|
|
15
|
+
fill="#1D1D1B"
|
|
16
|
+
/>
|
|
17
|
+
</svg>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { CkSendIcon, CkStopIcon } from './icons';
|
|
2
|
+
export { CkAgentMessage } from './CkAgentMessage';
|
|
3
|
+
export { CkConversationLayout } from './CkConversationLayout';
|
|
4
|
+
export type { CkConversationLayoutProps } from './CkConversationLayout';
|
|
5
|
+
export { CkCopyMessageActionButton } from './CkCopyMessageActionButton';
|
|
6
|
+
export { CkHistoryItem } from './CkHistoryItem';
|
|
7
|
+
export type { CkHistoryItemProps, CkHistoryItemEmits } from './CkHistoryItem';
|
|
8
|
+
export { CkHistoryWrapper } from './CkHistoryWrapper';
|
|
9
|
+
export type { CkHistoryWrapperProps } from './CkHistoryWrapper';
|
|
10
|
+
export { CkMarkdownRenderer } from './CkMarkdownRenderer';
|
|
11
|
+
export type {
|
|
12
|
+
CkMarkdownRendererProps,
|
|
13
|
+
CkMarkdownElementClasses,
|
|
14
|
+
} from './CkMarkdownRenderer';
|
|
15
|
+
export { CkMessageActionButton } from './CkMessageActionButton';
|
|
16
|
+
export { CkPromptActionButton } from './CkPromptActionButton';
|
|
17
|
+
export { CkPromptStopButton } from './CkPromptStopButton';
|
|
18
|
+
export { CkPromptInput } from './CkPromptInput';
|
|
19
|
+
export { CkPromptInputDisclaimer } from './CkPromptInputDisclaimer';
|
|
20
|
+
export { CkUserMessage } from './CkUserMessage';
|
|
21
|
+
export type { CkUserMessageProps } from './CkUserMessage';
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import type { BaseMessage } from '../../../types';
|
|
2
|
+
import CkConversation from './CkConversation.vue';
|
|
3
|
+
import { defaultMarkdownClasses } from '../../base/CkMarkdownRenderer/defaultMarkdownClasses';
|
|
4
|
+
import { render, screen } from '@testing-library/vue';
|
|
5
|
+
import { h } from 'vue';
|
|
6
|
+
|
|
7
|
+
const now = new Date();
|
|
8
|
+
|
|
9
|
+
const messages: BaseMessage[] = [
|
|
10
|
+
{
|
|
11
|
+
id: '1',
|
|
12
|
+
role: 'user',
|
|
13
|
+
content: 'Hello there',
|
|
14
|
+
createdAt: now,
|
|
15
|
+
status: 'completed',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: '2',
|
|
19
|
+
role: 'assistant',
|
|
20
|
+
content: 'Hi! How can I help?',
|
|
21
|
+
createdAt: now,
|
|
22
|
+
status: 'completed',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: '3',
|
|
26
|
+
role: 'user',
|
|
27
|
+
content: 'Tell me a joke',
|
|
28
|
+
createdAt: now,
|
|
29
|
+
status: 'completed',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: '4',
|
|
33
|
+
role: 'assistant',
|
|
34
|
+
content: 'Why did the chicken cross the road?',
|
|
35
|
+
createdAt: now,
|
|
36
|
+
status: 'completed',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function agentMessage(overrides: Partial<BaseMessage> = {}): BaseMessage {
|
|
41
|
+
return {
|
|
42
|
+
id: '1',
|
|
43
|
+
role: 'assistant',
|
|
44
|
+
content: 'Hello',
|
|
45
|
+
createdAt: now,
|
|
46
|
+
status: 'completed',
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('CkConversation', () => {
|
|
52
|
+
describe('default rendering', () => {
|
|
53
|
+
it('displays all user messages', () => {
|
|
54
|
+
render(CkConversation, { props: { messages } });
|
|
55
|
+
|
|
56
|
+
expect(screen.getByText('Hello there')).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText('Tell me a joke')).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('displays all agent messages', () => {
|
|
61
|
+
render(CkConversation, { props: { messages } });
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText('Hi! How can I help?')).toBeInTheDocument();
|
|
64
|
+
expect(
|
|
65
|
+
screen.getByText('Why did the chicken cross the road?'),
|
|
66
|
+
).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('agent icon', () => {
|
|
71
|
+
it('displays avatar fallback when agentAvatarFallback is provided', () => {
|
|
72
|
+
render(CkConversation, {
|
|
73
|
+
props: { messages: [agentMessage()], agentAvatarFallback: 'WB' },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(screen.getByText('WB')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('hides the icon area when no agentAvatarImage, no agentAvatarFallback, and no agent-message-icon slot', () => {
|
|
80
|
+
const { container } = render(CkConversation, {
|
|
81
|
+
props: { messages: [agentMessage()] },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// The icon container inside CkAgentMessage should not be rendered
|
|
85
|
+
const agentMessageEl = container.querySelector(
|
|
86
|
+
'.flex.w-full.items-start.gap-2',
|
|
87
|
+
);
|
|
88
|
+
expect(agentMessageEl).toBeInTheDocument();
|
|
89
|
+
// The flex-shrink-0 icon wrapper should not exist
|
|
90
|
+
expect(
|
|
91
|
+
agentMessageEl?.querySelector(
|
|
92
|
+
'.flex.flex-shrink-0.items-center.justify-center',
|
|
93
|
+
),
|
|
94
|
+
).not.toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('shows the icon area when agentAvatarFallback is provided', () => {
|
|
98
|
+
const { container } = render(CkConversation, {
|
|
99
|
+
props: { messages: [agentMessage()], agentAvatarFallback: 'AI' },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const agentMessageEl = container.querySelector(
|
|
103
|
+
'.flex.w-full.items-start.gap-2',
|
|
104
|
+
);
|
|
105
|
+
expect(
|
|
106
|
+
agentMessageEl?.querySelector(
|
|
107
|
+
'.flex.flex-shrink-0.items-center.justify-center',
|
|
108
|
+
),
|
|
109
|
+
).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('shows the icon area when agentAvatarImage URL is provided', () => {
|
|
113
|
+
const { container } = render(CkConversation, {
|
|
114
|
+
props: {
|
|
115
|
+
messages: [agentMessage()],
|
|
116
|
+
agentAvatarImage: 'https://example.com/icon.png',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const agentMessageEl = container.querySelector(
|
|
121
|
+
'.flex.w-full.items-start.gap-2',
|
|
122
|
+
);
|
|
123
|
+
expect(
|
|
124
|
+
agentMessageEl?.querySelector(
|
|
125
|
+
'.flex.flex-shrink-0.items-center.justify-center',
|
|
126
|
+
),
|
|
127
|
+
).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('shows the icon area when agent-message-icon slot is provided', () => {
|
|
131
|
+
const { container } = render(CkConversation, {
|
|
132
|
+
props: { messages: [agentMessage()] },
|
|
133
|
+
slots: {
|
|
134
|
+
'agent-message-icon': '<span data-testid="custom-icon">🤖</span>',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
|
|
139
|
+
const agentMessageEl = container.querySelector(
|
|
140
|
+
'.flex.w-full.items-start.gap-2',
|
|
141
|
+
);
|
|
142
|
+
expect(
|
|
143
|
+
agentMessageEl?.querySelector(
|
|
144
|
+
'.flex.flex-shrink-0.items-center.justify-center',
|
|
145
|
+
),
|
|
146
|
+
).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('loading icon', () => {
|
|
151
|
+
it('shows UiSpinner by default when message is loading and icon area is visible', () => {
|
|
152
|
+
render(CkConversation, {
|
|
153
|
+
props: {
|
|
154
|
+
messages: [agentMessage({ status: 'started' })],
|
|
155
|
+
agentAvatarImage: 'avatar.png',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(screen.getByLabelText('Agent loading')).toBeInTheDocument();
|
|
160
|
+
expect(screen.queryByAltText('Agent avatar')).not.toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('shows agentLoadingImage instead of UiSpinner when provided', () => {
|
|
164
|
+
render(CkConversation, {
|
|
165
|
+
props: {
|
|
166
|
+
messages: [agentMessage({ status: 'started' })],
|
|
167
|
+
agentAvatarImage: 'avatar.png',
|
|
168
|
+
agentLoadingImage: 'loading.png',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const img = screen.getByAltText('Agent loading');
|
|
173
|
+
expect(img).toBeInTheDocument();
|
|
174
|
+
expect(img).toHaveAttribute('src', 'loading.png');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('shows custom slot content instead of agentLoadingImage or UiSpinner', () => {
|
|
178
|
+
render(CkConversation, {
|
|
179
|
+
props: {
|
|
180
|
+
messages: [agentMessage({ status: 'sending' })],
|
|
181
|
+
agentAvatarImage: 'avatar.png',
|
|
182
|
+
agentLoadingImage: 'loading.png',
|
|
183
|
+
},
|
|
184
|
+
slots: {
|
|
185
|
+
'agent-message-loading-icon':
|
|
186
|
+
'<span data-testid="custom-loader">Loading…</span>',
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(screen.getByTestId('custom-loader')).toBeInTheDocument();
|
|
191
|
+
expect(screen.queryByAltText('Agent loading')).not.toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('shows the normal avatar icon when message is completed', () => {
|
|
195
|
+
render(CkConversation, {
|
|
196
|
+
props: {
|
|
197
|
+
messages: [agentMessage({ status: 'completed' })],
|
|
198
|
+
agentAvatarImage: 'avatar.png',
|
|
199
|
+
agentLoadingImage: 'loading.png',
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(screen.getByAltText('Agent avatar')).toBeInTheDocument();
|
|
204
|
+
expect(screen.queryByAltText('Agent loading')).not.toBeInTheDocument();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('shows UiSpinner for "sending" status as well', () => {
|
|
208
|
+
render(CkConversation, {
|
|
209
|
+
props: {
|
|
210
|
+
messages: [agentMessage({ status: 'sending' })],
|
|
211
|
+
agentAvatarImage: 'avatar.png',
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(screen.getByLabelText('Agent loading')).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('slot overrides', () => {
|
|
220
|
+
it('allows replacing user message content via user-message-content slot', () => {
|
|
221
|
+
render(CkConversation, {
|
|
222
|
+
props: {
|
|
223
|
+
messages: [
|
|
224
|
+
{
|
|
225
|
+
id: '1',
|
|
226
|
+
role: 'user',
|
|
227
|
+
content: 'Original',
|
|
228
|
+
createdAt: now,
|
|
229
|
+
status: 'completed',
|
|
230
|
+
} as BaseMessage,
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
slots: { 'user-message-content': 'Custom user content' },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(screen.getByText('Custom user content')).toBeInTheDocument();
|
|
237
|
+
expect(screen.queryByText('Original')).not.toBeInTheDocument();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('allows replacing the entire user message via user-message slot', () => {
|
|
241
|
+
render(CkConversation, {
|
|
242
|
+
props: {
|
|
243
|
+
messages: [
|
|
244
|
+
{
|
|
245
|
+
id: '1',
|
|
246
|
+
role: 'user',
|
|
247
|
+
content: 'Original',
|
|
248
|
+
createdAt: now,
|
|
249
|
+
status: 'completed',
|
|
250
|
+
} as BaseMessage,
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
slots: {
|
|
254
|
+
'user-message': '<div data-testid="custom-user">Totally custom</div>',
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(screen.getByTestId('custom-user')).toBeInTheDocument();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('allows replacing agent content via agent-message-content slot', () => {
|
|
262
|
+
render(CkConversation, {
|
|
263
|
+
props: { messages: [agentMessage({ content: 'Original' })] },
|
|
264
|
+
slots: { 'agent-message-content': 'Rich agent content' },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(screen.getByText('Rich agent content')).toBeInTheDocument();
|
|
268
|
+
expect(screen.queryByText('Original')).not.toBeInTheDocument();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('allows replacing the entire agent message via agent-message slot', () => {
|
|
272
|
+
render(CkConversation, {
|
|
273
|
+
props: { messages: [agentMessage({ content: 'Original' })] },
|
|
274
|
+
slots: {
|
|
275
|
+
'agent-message':
|
|
276
|
+
'<div data-testid="custom-agent">Fully replaced</div>',
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(screen.getByTestId('custom-agent')).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('allows replacing the agent icon via agent-message-icon slot', () => {
|
|
284
|
+
render(CkConversation, {
|
|
285
|
+
props: { messages: [agentMessage()] },
|
|
286
|
+
slots: { 'agent-message-icon': '<img alt="custom-bot" />' },
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(screen.getByAltText('custom-bot')).toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('allows adding a header via agent-message-header slot', () => {
|
|
293
|
+
render(CkConversation, {
|
|
294
|
+
props: { messages: [agentMessage()] },
|
|
295
|
+
slots: { 'agent-message-header': 'Custom Header' },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(screen.getByText('Custom Header')).toBeInTheDocument();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('allows adding footer actions via agent-message-footer slot', () => {
|
|
302
|
+
render(CkConversation, {
|
|
303
|
+
props: { messages: [agentMessage()] },
|
|
304
|
+
slots: { 'agent-message-footer': '<button>Copy</button>' },
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(screen.getByText('Copy')).toBeInTheDocument();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('empty conversation', () => {
|
|
312
|
+
it('renders nothing when messages array is empty', () => {
|
|
313
|
+
const { container } = render(CkConversation, { props: { messages: [] } });
|
|
314
|
+
|
|
315
|
+
// Only the scroll container should exist, with no message content
|
|
316
|
+
expect(container.querySelectorAll('[data-message-row]')).toHaveLength(0);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('markdown rendering', () => {
|
|
321
|
+
it('renders agent message content as markdown by default', () => {
|
|
322
|
+
const { container } = render(CkConversation, {
|
|
323
|
+
props: {
|
|
324
|
+
messages: [agentMessage({ content: 'This has **bold** text' })],
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(container.querySelector('strong')?.textContent).toBe('bold');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('renders markdown headings and lists', () => {
|
|
332
|
+
const { container } = render(CkConversation, {
|
|
333
|
+
props: {
|
|
334
|
+
messages: [
|
|
335
|
+
agentMessage({ content: '## Title\n\n- item one\n- item two' }),
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(container.querySelector('h2')?.textContent).toBe('Title');
|
|
341
|
+
expect(container.querySelectorAll('li')).toHaveLength(2);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('applies default markdown classes to elements', () => {
|
|
345
|
+
const { container } = render(CkConversation, {
|
|
346
|
+
props: {
|
|
347
|
+
messages: [agentMessage({ content: 'A paragraph' })],
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const p = container.querySelector('.markdown-renderer p');
|
|
352
|
+
expect(p).not.toBeNull();
|
|
353
|
+
expect(p?.className).toContain(defaultMarkdownClasses.p);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('merges agentMarkdownClasses prop with defaults', () => {
|
|
357
|
+
const { container } = render(CkConversation, {
|
|
358
|
+
props: {
|
|
359
|
+
messages: [agentMessage({ content: 'A paragraph' })],
|
|
360
|
+
agentMarkdownClasses: { p: 'custom-paragraph' },
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const p = container.querySelector('.markdown-renderer p');
|
|
365
|
+
expect(p?.className).toContain('custom-paragraph');
|
|
366
|
+
expect(p?.className).not.toContain(defaultMarkdownClasses.p);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('preserves default classes for elements not overridden', () => {
|
|
370
|
+
const { container } = render(CkConversation, {
|
|
371
|
+
props: {
|
|
372
|
+
messages: [
|
|
373
|
+
agentMessage({
|
|
374
|
+
content:
|
|
375
|
+
'## Heading\n\nParagraph with [link](https://example.com)',
|
|
376
|
+
}),
|
|
377
|
+
],
|
|
378
|
+
agentMarkdownClasses: { p: 'custom-paragraph' },
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const h2 = container.querySelector('.markdown-renderer h2');
|
|
383
|
+
expect(h2?.className).toContain(defaultMarkdownClasses.h2);
|
|
384
|
+
|
|
385
|
+
const a = container.querySelector('.markdown-renderer a');
|
|
386
|
+
expect(a?.className).toContain(defaultMarkdownClasses.a);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('does not render CkMarkdownRenderer when agent-message-content slot is provided', () => {
|
|
390
|
+
const { container } = render(CkConversation, {
|
|
391
|
+
props: { messages: [agentMessage({ content: '**bold**' })] },
|
|
392
|
+
slots: {
|
|
393
|
+
'agent-message-content':
|
|
394
|
+
'<div data-testid="custom-content">Custom</div>',
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(screen.getByTestId('custom-content')).toBeInTheDocument();
|
|
399
|
+
expect(container.querySelector('.markdown-renderer')).toBeNull();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('forwards agent-message-code-block slot to CkMarkdownRenderer', () => {
|
|
403
|
+
const markdown = '```python\nprint("hello")\n```';
|
|
404
|
+
|
|
405
|
+
const { container } = render(CkConversation, {
|
|
406
|
+
props: { messages: [agentMessage({ content: markdown })] },
|
|
407
|
+
slots: {
|
|
408
|
+
'agent-message-code-block': (props: {
|
|
409
|
+
code: string;
|
|
410
|
+
language: string;
|
|
411
|
+
}) =>
|
|
412
|
+
h(
|
|
413
|
+
'div',
|
|
414
|
+
{ class: 'custom-code', 'data-lang': props.language },
|
|
415
|
+
props.code,
|
|
416
|
+
),
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const custom = container.querySelector('.custom-code');
|
|
421
|
+
expect(custom).not.toBeNull();
|
|
422
|
+
expect(custom?.textContent).toContain('print("hello")');
|
|
423
|
+
expect(custom?.getAttribute('data-lang')).toBe('python');
|
|
424
|
+
expect(container.querySelector('pre > code')).toBeNull();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('renders default code block when agent-message-code-block slot is not provided', () => {
|
|
428
|
+
const markdown = '```javascript\nconsole.log("hi");\n```';
|
|
429
|
+
|
|
430
|
+
const { container } = render(CkConversation, {
|
|
431
|
+
props: { messages: [agentMessage({ content: markdown })] },
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const codeEl = container.querySelector('pre > code');
|
|
435
|
+
expect(codeEl).not.toBeNull();
|
|
436
|
+
expect(codeEl?.textContent).toContain('console.log("hi");');
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
});
|