@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 type { CkPromptInputProps } from './types';
3
+ import { computed, nextTick, useId, useTemplateRef, watch } from 'vue';
4
+
5
+ defineOptions({ name: 'CkPromptInput' });
6
+
7
+ const props = withDefaults(defineProps<CkPromptInputProps>(), {
8
+ modelValue: '',
9
+ placeholder: undefined,
10
+ disabled: false,
11
+ maxHeight: 200,
12
+ ariaLabel: undefined,
13
+ });
14
+
15
+ const emit = defineEmits<{
16
+ (e: 'update:modelValue', value: string): void;
17
+ (e: 'submit', value: string): void;
18
+ }>();
19
+
20
+ const textareaId = useId();
21
+ const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef');
22
+
23
+ const isEmpty = computed(() => props.modelValue.trim() === '');
24
+ const isSubmitDisabled = computed(() => props.disabled || isEmpty.value);
25
+
26
+ function adjustHeight(): void {
27
+ const el = textareaRef.value;
28
+ if (!el) return;
29
+ el.style.height = 'auto';
30
+ el.style.height = `${Math.min(el.scrollHeight, props.maxHeight)}px`;
31
+ }
32
+
33
+ function onInput(event: Event): void {
34
+ const value = (event.target as HTMLTextAreaElement).value;
35
+ emit('update:modelValue', value);
36
+ adjustHeight();
37
+ }
38
+
39
+ function submit(): void {
40
+ if (isSubmitDisabled.value) return;
41
+ const trimmed = props.modelValue.trim();
42
+ if (!trimmed) return;
43
+ emit('submit', trimmed);
44
+ }
45
+
46
+ function onKeydown(event: KeyboardEvent): void {
47
+ if (event.key === 'Enter' && !event.shiftKey) {
48
+ event.preventDefault();
49
+ submit();
50
+ }
51
+ }
52
+
53
+ function focus(): void {
54
+ textareaRef.value?.focus();
55
+ }
56
+
57
+ watch(
58
+ () => props.modelValue,
59
+ () => {
60
+ nextTick(adjustHeight);
61
+ },
62
+ );
63
+
64
+ defineExpose({ focus, submit });
65
+ </script>
66
+
67
+ <template>
68
+ <div class="flex flex-col gap-2">
69
+ <form
70
+ class="border-core-border-default bg-core-bg-primary [&:has(textarea:focus)]:ring-core-border-focus flex flex-col gap-3 rounded-xl border px-4 pt-3 pb-3 shadow-sm [&:has(textarea:focus)]:ring-2"
71
+ @submit.prevent="submit"
72
+ >
73
+ <label v-if="ariaLabel" :for="textareaId" class="sr-only">{{
74
+ ariaLabel
75
+ }}</label>
76
+ <textarea
77
+ :id="textareaId"
78
+ ref="textareaRef"
79
+ rows="2"
80
+ :value="modelValue"
81
+ :placeholder="placeholder"
82
+ :disabled="disabled"
83
+ class="text-core-content-primary placeholder-core-content-placeholder min-h-10 w-full resize-none bg-transparent text-sm leading-5 focus:outline-none"
84
+ @input="onInput"
85
+ @keydown="onKeydown"
86
+ />
87
+ <div class="flex items-center justify-between">
88
+ <div v-if="$slots['toolbar-left']">
89
+ <slot name="toolbar-left" />
90
+ </div>
91
+ <div v-else />
92
+ <div class="ml-auto">
93
+ <slot
94
+ name="toolbar-right"
95
+ :submit="submit"
96
+ :disabled="isSubmitDisabled"
97
+ :is-empty="isEmpty"
98
+ />
99
+ </div>
100
+ </div>
101
+ </form>
102
+ <slot name="footer" />
103
+ </div>
104
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkPromptInput } from './CkPromptInput.vue';
2
+ export type * from './types';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Low-level prompt input: a single auto-growing textarea with left/right toolbar
3
+ * slots, a footer slot, and keyboard submission (Enter to submit, Shift+Enter
4
+ * for newline). Use when you need full control over the prompt layout; for a
5
+ * turnkey chat input use `CkInput` instead.
6
+ *
7
+ * @category Form Inputs
8
+ * @useCases raw chat prompt textarea, custom prompt toolbar, auto-growing textarea, compose box, keyboard submit
9
+ * @keywords prompt, input, textarea, chat, auto-grow, compose, submit, v-model
10
+ * @related CkInput, CkPromptActionButton, CkPromptInputDisclaimer
11
+ */
12
+ export interface CkPromptInputProps {
13
+ /**
14
+ * Current value of the textarea. Supports `v-model`.
15
+ * @default ''
16
+ */
17
+ modelValue?: string;
18
+ /** Placeholder shown when the textarea is empty. */
19
+ placeholder?: string;
20
+ /**
21
+ * Whether the textarea is disabled.
22
+ * @default false
23
+ */
24
+ disabled?: boolean;
25
+ /**
26
+ * Maximum height (in px) before the textarea becomes scrollable.
27
+ * @default 200
28
+ */
29
+ maxHeight?: number;
30
+ /** Accessible label for the textarea, hidden visually but read by assistive tech. */
31
+ ariaLabel?: string;
32
+ }
33
+
34
+ export interface CkPromptInputEmits {
35
+ /** Emitted on every keystroke. */
36
+ (e: 'update:modelValue', value: string): void;
37
+ /** Emitted when the user submits via Enter (without Shift). */
38
+ (e: 'submit', value: string): void;
39
+ }
@@ -0,0 +1,40 @@
1
+ import CkPromptInputDisclaimer from './CkPromptInputDisclaimer.vue';
2
+ import { render, screen } from '@testing-library/vue';
3
+
4
+ describe('CkPromptInputDisclaimer', () => {
5
+ it('renders text from the text prop', () => {
6
+ render(CkPromptInputDisclaimer, {
7
+ props: { text: 'AI may make mistakes' },
8
+ });
9
+
10
+ expect(screen.getByText('AI may make mistakes')).toBeInTheDocument();
11
+ });
12
+
13
+ it('renders nothing when no text and no slot provided', () => {
14
+ const { container } = render(CkPromptInputDisclaimer);
15
+
16
+ expect(container.querySelector('p')).not.toBeInTheDocument();
17
+ });
18
+
19
+ it('renders slot content and overrides text prop', () => {
20
+ render(CkPromptInputDisclaimer, {
21
+ props: { text: 'Prop text' },
22
+ slots: { default: '<span>Slot content</span>' },
23
+ });
24
+
25
+ expect(screen.getByText('Slot content')).toBeInTheDocument();
26
+ expect(screen.queryByText('Prop text')).not.toBeInTheDocument();
27
+ });
28
+
29
+ it('applies correct styling classes', () => {
30
+ render(CkPromptInputDisclaimer, {
31
+ props: { text: 'Disclaimer' },
32
+ });
33
+
34
+ const el = screen.getByText('Disclaimer');
35
+ expect(el.tagName).toBe('P');
36
+ expect(el.className).toContain('text-core-content-tertiary');
37
+ expect(el.className).toContain('text-center');
38
+ expect(el.className).toContain('text-xs');
39
+ });
40
+ });
@@ -0,0 +1,103 @@
1
+ import CkPromptInputDisclaimer from './CkPromptInputDisclaimer.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ const meta: Meta<typeof CkPromptInputDisclaimer> = {
5
+ title: 'Base/CkPromptInputDisclaimer',
6
+ component: CkPromptInputDisclaimer,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ text: {
10
+ control: 'text',
11
+ description:
12
+ 'Disclaimer text to display. When omitted and the default slot is empty, nothing renders.',
13
+ },
14
+ },
15
+ decorators: [
16
+ () => ({
17
+ template:
18
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
19
+ }),
20
+ ],
21
+ };
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ const defaultTemplateSource = `<script setup lang="ts">
27
+ import { CkPromptInputDisclaimer } from '@aleph-alpha/chat-kit';
28
+ </script>
29
+
30
+ <template>
31
+ <CkPromptInputDisclaimer
32
+ text="Assistant Chat may occasionally make mistakes. Please, make sure to double-check important information."
33
+ />
34
+ </template>`;
35
+
36
+ const withSlotTemplateSource = `<script setup lang="ts">
37
+ import { CkPromptInputDisclaimer } from '@aleph-alpha/chat-kit';
38
+ </script>
39
+
40
+ <template>
41
+ <CkPromptInputDisclaimer>
42
+ <span>Custom disclaimer with <strong>rich</strong> content</span>
43
+ </CkPromptInputDisclaimer>
44
+ </template>`;
45
+
46
+ const emptyTemplateSource = `<script setup lang="ts">
47
+ import { CkPromptInputDisclaimer } from '@aleph-alpha/chat-kit';
48
+ </script>
49
+
50
+ <template>
51
+ <CkPromptInputDisclaimer />
52
+ </template>`;
53
+
54
+ /**
55
+ * Standard disclaimer rendered via the `text` prop. Use the default copy
56
+ * pattern for chat surfaces that rely on an LLM backend.
57
+ */
58
+ export const Default: Story = {
59
+ render: () => ({
60
+ components: { CkPromptInputDisclaimer },
61
+ template: `
62
+ <CkPromptInputDisclaimer
63
+ text="Assistant Chat may occasionally make mistakes. Please, make sure to double-check important information."
64
+ />
65
+ `,
66
+ }),
67
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
68
+ };
69
+
70
+ /**
71
+ * Uses the default slot to embed rich content (links, bold, icons) inside the
72
+ * disclaimer. Use when plain `text` isn't expressive enough.
73
+ */
74
+ export const WithSlot: Story = {
75
+ render: () => ({
76
+ components: { CkPromptInputDisclaimer },
77
+ template: `
78
+ <CkPromptInputDisclaimer>
79
+ <span>Custom disclaimer with <strong>rich</strong> content</span>
80
+ </CkPromptInputDisclaimer>
81
+ `,
82
+ }),
83
+ parameters: { docs: { source: { code: withSlotTemplateSource } } },
84
+ };
85
+
86
+ /**
87
+ * When no `text` and no slot content are provided the component renders nothing.
88
+ * Use to safely include the disclaimer conditionally without template branches.
89
+ */
90
+ export const Empty: Story = {
91
+ render: () => ({
92
+ components: { CkPromptInputDisclaimer },
93
+ template: `
94
+ <div>
95
+ <p style="color: gray; font-size: 12px; margin-bottom: 8px;">
96
+ (Nothing renders below when no text or slot is provided)
97
+ </p>
98
+ <CkPromptInputDisclaimer />
99
+ </div>
100
+ `,
101
+ }),
102
+ parameters: { docs: { source: { code: emptyTemplateSource } } },
103
+ };
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import type { CkPromptInputDisclaimerProps } from './types';
3
+
4
+ defineOptions({ name: 'CkPromptInputDisclaimer' });
5
+
6
+ defineProps<CkPromptInputDisclaimerProps>();
7
+ </script>
8
+
9
+ <template>
10
+ <p
11
+ v-if="$slots.default || text"
12
+ class="text-core-content-tertiary text-center text-xs leading-3.5"
13
+ >
14
+ <slot>{{ text }}</slot>
15
+ </p>
16
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkPromptInputDisclaimer } from './CkPromptInputDisclaimer.vue';
2
+ export type * from './types';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Small helper/disclaimer line typically rendered under a chat input ("AI may
3
+ * produce incorrect answers"). Renders nothing when there is no text and no
4
+ * default slot content, making it safe to always include.
5
+ *
6
+ * @category Feedback
7
+ * @useCases chat disclaimer, ai may make mistakes, prompt footnote, small help text under input
8
+ * @keywords disclaimer, notice, footer, help text, ai, prompt
9
+ * @related CkInput, CkPromptInput
10
+ */
11
+ export interface CkPromptInputDisclaimerProps {
12
+ /**
13
+ * Disclaimer text to display. If omitted, content is read from the default
14
+ * slot; if neither is provided, nothing renders.
15
+ */
16
+ text?: string;
17
+ }
@@ -0,0 +1,64 @@
1
+ import CkPromptStopButton from './CkPromptStopButton.vue';
2
+ import { render, screen, fireEvent } from '@testing-library/vue';
3
+
4
+ describe('CkPromptStopButton', () => {
5
+ it('renders icon slot content', () => {
6
+ render(CkPromptStopButton, {
7
+ slots: { default: '<span data-testid="icon"></span>' },
8
+ });
9
+
10
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
11
+ });
12
+
13
+ it('uses default accessible name', () => {
14
+ render(CkPromptStopButton);
15
+
16
+ expect(
17
+ screen.getByRole('button', { name: 'Stop generating' }),
18
+ ).toBeInTheDocument();
19
+ });
20
+
21
+ it('allows overriding ariaLabel', () => {
22
+ render(CkPromptStopButton, {
23
+ props: { ariaLabel: 'Stop' },
24
+ });
25
+
26
+ expect(screen.getByRole('button', { name: 'Stop' })).toBeInTheDocument();
27
+ });
28
+
29
+ it('is type button so it does not submit a parent form', () => {
30
+ render(CkPromptStopButton);
31
+
32
+ expect(
33
+ screen.getByRole('button', { name: 'Stop generating' }),
34
+ ).toHaveAttribute('type', 'button');
35
+ });
36
+
37
+ it('emits click when pressed', async () => {
38
+ const { emitted } = render(CkPromptStopButton);
39
+
40
+ await fireEvent.click(
41
+ screen.getByRole('button', { name: 'Stop generating' }),
42
+ );
43
+
44
+ expect(emitted().click).toBeTruthy();
45
+ });
46
+
47
+ it('respects disabled prop', () => {
48
+ render(CkPromptStopButton, { props: { disabled: true } });
49
+
50
+ expect(
51
+ screen.getByRole('button', { name: 'Stop generating' }),
52
+ ).toBeDisabled();
53
+ });
54
+
55
+ it('applies brand styling classes', () => {
56
+ render(CkPromptStopButton);
57
+
58
+ const button = screen.getByRole('button', { name: 'Stop generating' });
59
+ expect(button.className).toContain('!bg-brand-bg-brand');
60
+ expect(button.className).toContain('hover:!bg-brand-bg-brand-hover');
61
+ expect(button.className).toContain('!text-brand-content-default');
62
+ expect(button.className).toContain('!rounded-md');
63
+ });
64
+ });
@@ -0,0 +1,81 @@
1
+ import CkStopIcon from '../icons/CkStopIcon.vue';
2
+ import CkPromptStopButton from './CkPromptStopButton.vue';
3
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
4
+
5
+ const meta: Meta<typeof CkPromptStopButton> = {
6
+ title: 'Base/CkPromptStopButton',
7
+ component: CkPromptStopButton,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ ariaLabel: {
11
+ control: 'text',
12
+ description:
13
+ 'Accessible label for the button. Required because the button is icon-only.',
14
+ },
15
+ disabled: {
16
+ control: 'boolean',
17
+ description: 'Whether the button is disabled.',
18
+ },
19
+ },
20
+ decorators: [
21
+ () => ({
22
+ template:
23
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
24
+ }),
25
+ ],
26
+ };
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof meta>;
30
+
31
+ const defaultTemplateSource = `<script setup lang="ts">
32
+ import { CkPromptStopButton, CkStopIcon } from '@aleph-alpha/chat-kit';
33
+ </script>
34
+
35
+ <template>
36
+ <CkPromptStopButton ariaLabel="Stop generating">
37
+ <CkStopIcon />
38
+ </CkPromptStopButton>
39
+ </template>`;
40
+
41
+ const disabledTemplateSource = `<script setup lang="ts">
42
+ import { CkPromptStopButton, CkStopIcon } from '@aleph-alpha/chat-kit';
43
+ </script>
44
+
45
+ <template>
46
+ <CkPromptStopButton ariaLabel="Stop generating" disabled>
47
+ <CkStopIcon />
48
+ </CkPromptStopButton>
49
+ </template>`;
50
+
51
+ /**
52
+ * Default stop button. Swap it in for `CkPromptActionButton` while an
53
+ * assistant reply is streaming so users can cancel the response.
54
+ */
55
+ export const Default: Story = {
56
+ render: () => ({
57
+ components: { CkPromptStopButton, CkStopIcon },
58
+ template: `
59
+ <CkPromptStopButton ariaLabel="Stop generating">
60
+ <CkStopIcon />
61
+ </CkPromptStopButton>
62
+ `,
63
+ }),
64
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
65
+ };
66
+
67
+ /**
68
+ * Disabled state — clicks are ignored. Use briefly during the request
69
+ * teardown to indicate the cancel was acknowledged.
70
+ */
71
+ export const Disabled: Story = {
72
+ render: () => ({
73
+ components: { CkPromptStopButton, CkStopIcon },
74
+ template: `
75
+ <CkPromptStopButton ariaLabel="Stop generating" disabled>
76
+ <CkStopIcon />
77
+ </CkPromptStopButton>
78
+ `,
79
+ }),
80
+ parameters: { docs: { source: { code: disabledTemplateSource } } },
81
+ };
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { UiButton } from '@aleph-alpha/ui-library';
3
+ import type { CkPromptStopButtonProps } from './types';
4
+
5
+ defineOptions({ name: 'CkPromptStopButton' });
6
+
7
+ const props = withDefaults(defineProps<CkPromptStopButtonProps>(), {
8
+ ariaLabel: 'Stop generating',
9
+ disabled: false,
10
+ });
11
+
12
+ defineEmits<{
13
+ (e: 'click', event: MouseEvent): void;
14
+ }>();
15
+ </script>
16
+
17
+ <template>
18
+ <UiButton
19
+ v-bind="{ ariaLabel }"
20
+ size="icon-sm"
21
+ :disabled="props.disabled"
22
+ class="!bg-brand-bg-brand hover:!bg-brand-bg-brand-hover !text-brand-content-default !rounded-md"
23
+ @click="$emit('click', $event)"
24
+ >
25
+ <slot />
26
+ </UiButton>
27
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkPromptStopButton } from './CkPromptStopButton.vue';
2
+ export type * from './types';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Icon-only button used to stop an in-flight assistant generation. Shares the
3
+ * brand styling of `CkPromptActionButton` but is intended to replace it in the
4
+ * prompt toolbar while a response is streaming.
5
+ *
6
+ * @category Actions
7
+ * @useCases stop assistant generation, cancel streaming reply, abort in-flight request, halt llm response
8
+ * @keywords stop, cancel, abort, prompt, action, button, icon, streaming
9
+ * @related CkPromptActionButton, CkPromptInput, CkInput
10
+ */
11
+ export interface CkPromptStopButtonProps {
12
+ /**
13
+ * Accessible label for the button. Required because the button only renders
14
+ * an icon.
15
+ * @default 'Stop generating'
16
+ */
17
+ ariaLabel?: string;
18
+ /**
19
+ * Whether the button is disabled and ignores clicks.
20
+ * @default false
21
+ */
22
+ disabled?: boolean;
23
+ }
24
+
25
+ export interface CkPromptStopButtonEmits {
26
+ /** Emitted when the user clicks the stop button. */
27
+ (e: 'click', event: MouseEvent): void;
28
+ }
@@ -0,0 +1,20 @@
1
+ import CkUserMessage from './CkUserMessage.vue';
2
+ import { render, screen } from '@testing-library/vue';
3
+
4
+ describe('CkUserMessage', () => {
5
+ it('displays the message text from prop', () => {
6
+ render(CkUserMessage, { props: { message: 'Hello from tests' } });
7
+
8
+ expect(screen.getByText('Hello from tests')).toBeInTheDocument();
9
+ });
10
+
11
+ it('displays custom slot content instead of the prop', () => {
12
+ render(CkUserMessage, {
13
+ props: { message: 'Fallback text' },
14
+ slots: { default: 'Custom slot content' },
15
+ });
16
+
17
+ expect(screen.getByText('Custom slot content')).toBeInTheDocument();
18
+ expect(screen.queryByText('Fallback text')).not.toBeInTheDocument();
19
+ });
20
+ });
@@ -0,0 +1,112 @@
1
+ import CkUserMessage from './CkUserMessage.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ const meta: Meta<typeof CkUserMessage> = {
5
+ title: 'Base/CkUserMessage',
6
+ component: CkUserMessage,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ message: {
10
+ control: 'text',
11
+ description:
12
+ 'The message text to display. Can also be provided via the default slot.',
13
+ },
14
+ },
15
+ decorators: [
16
+ () => ({
17
+ template:
18
+ '<div style="display: flex; flex-direction: column; align-items: flex-end; width: 100%;"><story /></div>',
19
+ }),
20
+ ],
21
+ };
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ const defaultTemplateSource = `<script setup lang="ts">
27
+ import { CkUserMessage } from '@aleph-alpha/chat-kit';
28
+ </script>
29
+
30
+ <template>
31
+ <CkUserMessage message="Hello! This is a user message." />
32
+ </template>`;
33
+
34
+ const longMessageTemplateSource = `<script setup lang="ts">
35
+ import { CkUserMessage } from '@aleph-alpha/chat-kit';
36
+ </script>
37
+
38
+ <template>
39
+ <CkUserMessage
40
+ message="Find the lessons learned and best practices from the 'Digital Transformation Phase 1' project."
41
+ />
42
+ </template>`;
43
+
44
+ const multilineMessageTemplateSource = `<script setup lang="ts">
45
+ import { CkUserMessage } from '@aleph-alpha/chat-kit';
46
+ </script>
47
+
48
+ <template>
49
+ <CkUserMessage
50
+ :message="\`First line of the message.\nSecond line of the message.\nThird line with more detail.\`"
51
+ />
52
+ </template>`;
53
+
54
+ const withSlotContentTemplateSource = `<script setup lang="ts">
55
+ import { CkUserMessage } from '@aleph-alpha/chat-kit';
56
+ </script>
57
+
58
+ <template>
59
+ <CkUserMessage message="">
60
+ <strong>Bold slot content</strong> with <em>rich formatting</em>.
61
+ </CkUserMessage>
62
+ </template>`;
63
+
64
+ /**
65
+ * Standard user message bubble populated via the `message` prop. Use as the
66
+ * default way to render the user turn in a conversation.
67
+ */
68
+ export const Default: Story = {
69
+ args: {
70
+ message: 'Hello! This is a user message.',
71
+ },
72
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
73
+ };
74
+
75
+ /**
76
+ * Longer text — demonstrates the bubble's max-width and line-wrapping behavior.
77
+ */
78
+ export const LongMessage: Story = {
79
+ args: {
80
+ message:
81
+ "Find the lessons learned and best practices from the 'Digital Transformation Phase 1' project, specifically focusing on process optimization and digital tool integration.",
82
+ },
83
+ parameters: { docs: { source: { code: longMessageTemplateSource } } },
84
+ };
85
+
86
+ /**
87
+ * Message containing explicit newlines. Use when the source content already
88
+ * includes line breaks.
89
+ */
90
+ export const MultilineMessage: Story = {
91
+ args: {
92
+ message:
93
+ 'First line of the message.\nSecond line of the message.\nThird line with more detail.',
94
+ },
95
+ parameters: { docs: { source: { code: multilineMessageTemplateSource } } },
96
+ };
97
+
98
+ /**
99
+ * Replaces the message body with arbitrary slot content (rich text, links,
100
+ * attachments). Use when a plain string is not sufficient.
101
+ */
102
+ export const WithSlotContent: Story = {
103
+ render: () => ({
104
+ components: { CkUserMessage },
105
+ template: `
106
+ <CkUserMessage message="">
107
+ <strong>Bold slot content</strong> with <em>rich formatting</em>.
108
+ </CkUserMessage>
109
+ `,
110
+ }),
111
+ parameters: { docs: { source: { code: withSlotContentTemplateSource } } },
112
+ };