@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,52 @@
1
+ import CkPromptActionButton from './CkPromptActionButton.vue';
2
+ import { render, screen } from '@testing-library/vue';
3
+
4
+ describe('CkPromptActionButton', () => {
5
+ it('renders icon slot content', () => {
6
+ render(CkPromptActionButton, {
7
+ props: { ariaLabel: 'Send' },
8
+ slots: { default: '<svg data-testid="icon"></svg>' },
9
+ });
10
+
11
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
12
+ });
13
+
14
+ it('passes ariaLabel to the underlying button', () => {
15
+ render(CkPromptActionButton, {
16
+ props: { ariaLabel: 'Send message' },
17
+ });
18
+
19
+ expect(
20
+ screen.getByRole('button', { name: 'Send message' }),
21
+ ).toBeInTheDocument();
22
+ });
23
+
24
+ it('passes disabled state to the underlying button', () => {
25
+ render(CkPromptActionButton, {
26
+ props: { ariaLabel: 'Send', disabled: true },
27
+ });
28
+
29
+ expect(screen.getByRole('button', { name: 'Send' })).toBeDisabled();
30
+ });
31
+
32
+ it('defaults type to button', () => {
33
+ render(CkPromptActionButton, {
34
+ props: { ariaLabel: 'Send' },
35
+ });
36
+
37
+ expect(screen.getByRole('button', { name: 'Send' })).toHaveAttribute(
38
+ 'type',
39
+ 'button',
40
+ );
41
+ });
42
+
43
+ it('applies brand styling classes', () => {
44
+ render(CkPromptActionButton, {
45
+ props: { ariaLabel: 'Send' },
46
+ });
47
+
48
+ const button = screen.getByRole('button', { name: 'Send' });
49
+ expect(button.className).toContain('!bg-brand-bg-brand');
50
+ expect(button.className).toContain('!rounded-md');
51
+ });
52
+ });
@@ -0,0 +1,126 @@
1
+ import CkPromptActionButton from './CkPromptActionButton.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ const meta: Meta<typeof CkPromptActionButton> = {
5
+ title: 'Base/CkPromptActionButton',
6
+ component: CkPromptActionButton,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ ariaLabel: {
10
+ control: 'text',
11
+ description:
12
+ 'Accessible label for the button. Required because the button is icon-only.',
13
+ },
14
+ disabled: {
15
+ control: 'boolean',
16
+ description: 'Whether the button is disabled.',
17
+ },
18
+ loading: {
19
+ control: 'boolean',
20
+ description:
21
+ 'Shows a spinner instead of the slotted icon and blocks clicks.',
22
+ },
23
+ type: {
24
+ control: 'radio',
25
+ options: ['button', 'submit'],
26
+ description: 'Native button type. Use `submit` inside a <form>.',
27
+ },
28
+ },
29
+ decorators: [
30
+ () => ({
31
+ template:
32
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
33
+ }),
34
+ ],
35
+ };
36
+
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ const defaultTemplateSource = `<script setup lang="ts">
41
+ import { CkPromptActionButton } from '@aleph-alpha/chat-kit';
42
+ import { CkSendIcon } from '@aleph-alpha/chat-kit';
43
+ </script>
44
+
45
+ <template>
46
+ <CkPromptActionButton ariaLabel="Send message">
47
+ <CkSendIcon />
48
+ </CkPromptActionButton>
49
+ </template>`;
50
+
51
+ const disabledTemplateSource = `<script setup lang="ts">
52
+ import { CkPromptActionButton, CkSendIcon } from '@aleph-alpha/chat-kit';
53
+ </script>
54
+
55
+ <template>
56
+ <CkPromptActionButton ariaLabel="Send message" disabled>
57
+ <CkSendIcon />
58
+ </CkPromptActionButton>
59
+ </template>`;
60
+
61
+ const loadingTemplateSource = `<script setup lang="ts">
62
+ import { CkPromptActionButton, CkSendIcon } from '@aleph-alpha/chat-kit';
63
+ </script>
64
+
65
+ <template>
66
+ <CkPromptActionButton ariaLabel="Send message" loading>
67
+ <CkSendIcon />
68
+ </CkPromptActionButton>
69
+ </template>`;
70
+
71
+ /**
72
+ * Default send-style button. Use in a prompt input toolbar to submit a message
73
+ * or trigger a primary chat action.
74
+ */
75
+ export const Default: Story = {
76
+ render: () => ({
77
+ components: { CkPromptActionButton },
78
+ template: `
79
+ <CkPromptActionButton ariaLabel="Send message">
80
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
81
+ <path 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" />
82
+ <path d="M6 12h16" />
83
+ </svg>
84
+ </CkPromptActionButton>
85
+ `,
86
+ }),
87
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
88
+ };
89
+
90
+ /**
91
+ * Disabled state — use when the input is empty or a submit is otherwise
92
+ * blocked (e.g. rate-limited).
93
+ */
94
+ export const Disabled: Story = {
95
+ render: () => ({
96
+ components: { CkPromptActionButton },
97
+ template: `
98
+ <CkPromptActionButton ariaLabel="Send message" disabled>
99
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
100
+ <path 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" />
101
+ <path d="M6 12h16" />
102
+ </svg>
103
+ </CkPromptActionButton>
104
+ `,
105
+ }),
106
+ parameters: { docs: { source: { code: disabledTemplateSource } } },
107
+ };
108
+
109
+ /**
110
+ * Loading state — shows a spinner and blocks clicks. Use while the previous
111
+ * message is still streaming or the request is in flight.
112
+ */
113
+ export const Loading: Story = {
114
+ render: () => ({
115
+ components: { CkPromptActionButton },
116
+ template: `
117
+ <CkPromptActionButton ariaLabel="Send message" loading>
118
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
119
+ <path 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" />
120
+ <path d="M6 12h16" />
121
+ </svg>
122
+ </CkPromptActionButton>
123
+ `,
124
+ }),
125
+ parameters: { docs: { source: { code: loadingTemplateSource } } },
126
+ };
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import type { CkPromptActionButtonProps } from './types';
3
+ import { UiButton } from '@aleph-alpha/ui-library';
4
+
5
+ defineOptions({ name: 'CkPromptActionButton' });
6
+
7
+ withDefaults(defineProps<CkPromptActionButtonProps>(), {
8
+ disabled: false,
9
+ loading: false,
10
+ type: 'button',
11
+ });
12
+ </script>
13
+
14
+ <template>
15
+ <UiButton
16
+ v-bind="{ ariaLabel }"
17
+ size="icon"
18
+ :type="type"
19
+ :disabled="disabled"
20
+ :loading="loading"
21
+ class="!bg-brand-bg-brand hover:!bg-brand-bg-brand-hover !text-brand-content-default !rounded-md"
22
+ >
23
+ <slot />
24
+ </UiButton>
25
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkPromptActionButton } from './CkPromptActionButton.vue';
2
+ export type * from './types';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Icon-only call-to-action button styled for the chat prompt area (brand color).
3
+ * Typically used as the "send" button inside a prompt input toolbar, but works
4
+ * for any primary prompt-level action.
5
+ *
6
+ * @category Actions
7
+ * @useCases send message button, submit prompt, primary chat action, loading send button, disabled send button
8
+ * @keywords prompt, send, action, button, icon, submit, loading, brand
9
+ * @related CkPromptInput, CkInput, CkMessageActionButton
10
+ */
11
+ export interface CkPromptActionButtonProps {
12
+ /**
13
+ * Accessible label for the button. **Required** because the button only
14
+ * renders an icon.
15
+ */
16
+ ariaLabel: string;
17
+ /**
18
+ * Whether the button is disabled.
19
+ * @default false
20
+ */
21
+ disabled?: boolean;
22
+ /**
23
+ * Whether to show a loading spinner instead of the slotted icon.
24
+ * @default false
25
+ */
26
+ loading?: boolean;
27
+ /**
28
+ * Native button `type`. Set `'submit'` when placed inside a `<form>`.
29
+ * @default 'button'
30
+ */
31
+ type?: 'button' | 'submit';
32
+ }
@@ -0,0 +1,181 @@
1
+ import CkPromptInput from './CkPromptInput.vue';
2
+ import { render, screen, fireEvent } from '@testing-library/vue';
3
+
4
+ describe('CkPromptInput', () => {
5
+ describe('rendering', () => {
6
+ it('renders with custom placeholder', () => {
7
+ render(CkPromptInput, { props: { placeholder: 'Type here...' } });
8
+
9
+ expect(screen.getByPlaceholderText('Type here...')).toBeInTheDocument();
10
+ });
11
+
12
+ it('renders without placeholder when not provided', () => {
13
+ const { container } = render(CkPromptInput);
14
+
15
+ const textarea = container.querySelector('textarea');
16
+ expect(textarea).not.toHaveAttribute('placeholder');
17
+ });
18
+
19
+ it('renders a form element', () => {
20
+ const { container } = render(CkPromptInput);
21
+
22
+ expect(container.querySelector('form')).toBeInTheDocument();
23
+ });
24
+
25
+ it('does not render a label when ariaLabel is not provided', () => {
26
+ const { container } = render(CkPromptInput);
27
+
28
+ expect(container.querySelector('label')).not.toBeInTheDocument();
29
+ });
30
+ });
31
+
32
+ describe('v-model', () => {
33
+ it('displays the modelValue in the textarea', () => {
34
+ render(CkPromptInput, { props: { modelValue: 'Hello world' } });
35
+
36
+ expect(screen.getByRole('textbox')).toHaveValue('Hello world');
37
+ });
38
+
39
+ it('emits update:modelValue on input', async () => {
40
+ const { emitted } = render(CkPromptInput);
41
+
42
+ await fireEvent.update(screen.getByRole('textbox'), 'New text');
43
+
44
+ expect(emitted()['update:modelValue']).toBeTruthy();
45
+ expect(emitted()['update:modelValue'][0]).toEqual(['New text']);
46
+ });
47
+ });
48
+
49
+ describe('submit', () => {
50
+ it('emits submit with trimmed text on Enter key', async () => {
51
+ const { emitted } = render(CkPromptInput, {
52
+ props: { modelValue: ' Hello ' },
53
+ });
54
+
55
+ await fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' });
56
+
57
+ expect(emitted().submit).toBeTruthy();
58
+ expect(emitted().submit[0]).toEqual(['Hello']);
59
+ });
60
+
61
+ it('does not emit submit on Shift+Enter', async () => {
62
+ const { emitted } = render(CkPromptInput, {
63
+ props: { modelValue: 'Hello' },
64
+ });
65
+
66
+ await fireEvent.keyDown(screen.getByRole('textbox'), {
67
+ key: 'Enter',
68
+ shiftKey: true,
69
+ });
70
+
71
+ expect(emitted().submit).toBeFalsy();
72
+ });
73
+
74
+ it('does not emit submit when textarea is empty', async () => {
75
+ const { emitted } = render(CkPromptInput, {
76
+ props: { modelValue: '' },
77
+ });
78
+
79
+ await fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' });
80
+
81
+ expect(emitted().submit).toBeFalsy();
82
+ });
83
+
84
+ it('does not emit submit when textarea contains only whitespace', async () => {
85
+ const { emitted } = render(CkPromptInput, {
86
+ props: { modelValue: ' ' },
87
+ });
88
+
89
+ await fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' });
90
+
91
+ expect(emitted().submit).toBeFalsy();
92
+ });
93
+
94
+ it('does not emit submit when disabled', async () => {
95
+ const { emitted } = render(CkPromptInput, {
96
+ props: { modelValue: 'Hello', disabled: true },
97
+ });
98
+
99
+ await fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' });
100
+
101
+ expect(emitted().submit).toBeFalsy();
102
+ });
103
+
104
+ it('emits submit on form submission', async () => {
105
+ const { container, emitted } = render(CkPromptInput, {
106
+ props: { modelValue: 'Hello' },
107
+ });
108
+
109
+ await fireEvent.submit(container.querySelector('form')!);
110
+
111
+ expect(emitted().submit).toBeTruthy();
112
+ expect(emitted().submit[0]).toEqual(['Hello']);
113
+ });
114
+ });
115
+
116
+ describe('slots', () => {
117
+ it('renders toolbar-left slot content when provided', () => {
118
+ render(CkPromptInput, {
119
+ slots: { 'toolbar-left': '<button>Express</button>' },
120
+ });
121
+
122
+ expect(screen.getByText('Express')).toBeInTheDocument();
123
+ });
124
+
125
+ it('does not render toolbar-left container when slot is not provided', () => {
126
+ const { container } = render(CkPromptInput);
127
+
128
+ const toolbarDivs = container.querySelectorAll(
129
+ 'form > div:last-child > div',
130
+ );
131
+ expect(toolbarDivs[0]?.children.length).toBe(0);
132
+ });
133
+
134
+ it('renders toolbar-right slot content', () => {
135
+ render(CkPromptInput, {
136
+ slots: { 'toolbar-right': '<button>Custom Send</button>' },
137
+ });
138
+
139
+ expect(screen.getByText('Custom Send')).toBeInTheDocument();
140
+ });
141
+
142
+ it('renders nothing in toolbar-right by default', () => {
143
+ const { container } = render(CkPromptInput);
144
+
145
+ const toolbarRight = container.querySelector(
146
+ 'form > div:last-child > div.ml-auto',
147
+ );
148
+ expect(toolbarRight?.children.length).toBe(0);
149
+ });
150
+
151
+ it('renders footer slot content', () => {
152
+ render(CkPromptInput, {
153
+ slots: { footer: '<span>Custom footer</span>' },
154
+ });
155
+
156
+ expect(screen.getByText('Custom footer')).toBeInTheDocument();
157
+ });
158
+
159
+ it('renders nothing in footer by default', () => {
160
+ const { container } = render(CkPromptInput);
161
+
162
+ expect(container.querySelector('p')).not.toBeInTheDocument();
163
+ });
164
+ });
165
+
166
+ describe('accessibility', () => {
167
+ it('has a visually hidden label linked to the textarea', () => {
168
+ render(CkPromptInput, { props: { ariaLabel: 'Message input' } });
169
+
170
+ expect(screen.getByLabelText('Message input')).toBeInTheDocument();
171
+ });
172
+ });
173
+
174
+ describe('disabled state', () => {
175
+ it('disables the textarea when disabled prop is true', () => {
176
+ render(CkPromptInput, { props: { disabled: true } });
177
+
178
+ expect(screen.getByRole('textbox')).toBeDisabled();
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,224 @@
1
+ import CkPromptInput from './CkPromptInput.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+ import { ref } from 'vue';
4
+
5
+ const meta: Meta<typeof CkPromptInput> = {
6
+ title: 'Base/CkPromptInput',
7
+ component: CkPromptInput,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ modelValue: {
11
+ control: 'text',
12
+ description: 'Current value of the textarea. Use with `v-model`.',
13
+ },
14
+ placeholder: {
15
+ control: 'text',
16
+ description: 'Placeholder shown when the textarea is empty.',
17
+ },
18
+ disabled: { control: 'boolean', description: 'Disables the textarea.' },
19
+ maxHeight: {
20
+ control: 'number',
21
+ description: 'Max height (px) before the textarea becomes scrollable.',
22
+ },
23
+ ariaLabel: {
24
+ control: 'text',
25
+ description: 'Accessible label for the textarea.',
26
+ },
27
+ },
28
+ decorators: [
29
+ () => ({
30
+ template:
31
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
32
+ }),
33
+ ],
34
+ };
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof meta>;
38
+
39
+ const defaultTemplateSource = `<script setup lang="ts">
40
+ import { CkPromptInput } from '@aleph-alpha/chat-kit';
41
+ import { ref } from 'vue';
42
+
43
+ const text = ref('');
44
+ </script>
45
+
46
+ <template>
47
+ <CkPromptInput
48
+ v-model="text"
49
+ placeholder="Type a message..."
50
+ aria-label="Message input"
51
+ />
52
+ </template>`;
53
+
54
+ const withToolbarSlotsTemplateSource = `<script setup lang="ts">
55
+ import { CkPromptInput } from '@aleph-alpha/chat-kit';
56
+ import { ref } from 'vue';
57
+
58
+ const text = ref('');
59
+ </script>
60
+
61
+ <template>
62
+ <CkPromptInput
63
+ v-model="text"
64
+ placeholder="Type a message..."
65
+ aria-label="Message input"
66
+ >
67
+ <template #toolbar-left>
68
+ <button class="h-9 rounded-md border px-3 text-sm">Options</button>
69
+ </template>
70
+ <template #toolbar-right="{ submit, disabled }">
71
+ <button :disabled="disabled" class="size-9 rounded-md bg-brand-bg-brand" @click="submit">
72
+ Send
73
+ </button>
74
+ </template>
75
+ </CkPromptInput>
76
+ </template>`;
77
+
78
+ const withFooterSlotTemplateSource = `<script setup lang="ts">
79
+ import { CkPromptInput } from '@aleph-alpha/chat-kit';
80
+ import { ref } from 'vue';
81
+
82
+ const text = ref('');
83
+ </script>
84
+
85
+ <template>
86
+ <CkPromptInput v-model="text" placeholder="Type a message..." aria-label="Message input">
87
+ <template #footer>
88
+ <p class="text-center text-xs text-core-content-tertiary">
89
+ Custom footer content via slot
90
+ </p>
91
+ </template>
92
+ </CkPromptInput>
93
+ </template>`;
94
+
95
+ const disabledTemplateSource = `<script setup lang="ts">
96
+ import { CkPromptInput } from '@aleph-alpha/chat-kit';
97
+ import { ref } from 'vue';
98
+
99
+ const text = ref('');
100
+ </script>
101
+
102
+ <template>
103
+ <CkPromptInput
104
+ v-model="text"
105
+ disabled
106
+ placeholder="Input is disabled"
107
+ aria-label="Disabled input"
108
+ />
109
+ </template>`;
110
+
111
+ /**
112
+ * Minimal auto-growing prompt textarea with `v-model` and Enter-to-submit.
113
+ * Use as the building block when composing a custom prompt input layout.
114
+ */
115
+ export const Default: Story = {
116
+ render: () => ({
117
+ components: { CkPromptInput },
118
+ setup() {
119
+ const text = ref('');
120
+ return { text };
121
+ },
122
+ template: `
123
+ <CkPromptInput
124
+ v-model="text"
125
+ placeholder="Type a message..."
126
+ aria-label="Message input"
127
+ />
128
+ `,
129
+ }),
130
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
131
+ };
132
+
133
+ /**
134
+ * Demonstrates both `toolbar-left` and `toolbar-right` slots. The right slot
135
+ * receives `{ submit, disabled, isEmpty }` to wire up a custom send button.
136
+ */
137
+ export const WithToolbarSlots: Story = {
138
+ render: () => ({
139
+ components: { CkPromptInput },
140
+ setup() {
141
+ const text = ref('');
142
+ return { text };
143
+ },
144
+ template: `
145
+ <CkPromptInput
146
+ v-model="text"
147
+ placeholder="Type a message..."
148
+ aria-label="Message input"
149
+ >
150
+ <template #toolbar-left>
151
+ <button
152
+ class="flex items-center gap-1 rounded-md border border-solid px-3 h-9 text-sm"
153
+ style="border-color: rgba(183, 180, 164, 0.3);"
154
+ >
155
+ Options
156
+ </button>
157
+ </template>
158
+ <template #toolbar-right="{ submit, disabled }">
159
+ <button
160
+ :disabled="disabled"
161
+ class="flex items-center justify-center size-9 rounded-md disabled:opacity-50"
162
+ style="background-color: #c9e400; color: #1d1d1b;"
163
+ @click="submit"
164
+ >
165
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path 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"/><path d="M6 12h16"/></svg>
166
+ </button>
167
+ </template>
168
+ </CkPromptInput>
169
+ `,
170
+ }),
171
+ parameters: { docs: { source: { code: withToolbarSlotsTemplateSource } } },
172
+ };
173
+
174
+ /**
175
+ * Uses the `footer` slot to render helper text (disclaimer, keyboard hint,
176
+ * etc.) directly below the input form. Use when you want a custom footer
177
+ * instead of `CkPromptInputDisclaimer`.
178
+ */
179
+ export const WithFooterSlot: Story = {
180
+ render: () => ({
181
+ components: { CkPromptInput },
182
+ setup() {
183
+ const text = ref('');
184
+ return { text };
185
+ },
186
+ template: `
187
+ <CkPromptInput
188
+ v-model="text"
189
+ placeholder="Type a message..."
190
+ aria-label="Message input"
191
+ >
192
+ <template #footer>
193
+ <p class="text-core-content-tertiary text-center text-xs leading-3.5">
194
+ Custom footer content via slot
195
+ </p>
196
+ </template>
197
+ </CkPromptInput>
198
+ `,
199
+ }),
200
+ parameters: { docs: { source: { code: withFooterSlotTemplateSource } } },
201
+ };
202
+
203
+ /**
204
+ * Disabled state — the textarea cannot be edited and Enter does not submit.
205
+ * Use while the server is processing the previous message.
206
+ */
207
+ export const Disabled: Story = {
208
+ render: () => ({
209
+ components: { CkPromptInput },
210
+ setup() {
211
+ const text = ref('');
212
+ return { text };
213
+ },
214
+ template: `
215
+ <CkPromptInput
216
+ v-model="text"
217
+ disabled
218
+ placeholder="Input is disabled"
219
+ aria-label="Disabled input"
220
+ />
221
+ `,
222
+ }),
223
+ parameters: { docs: { source: { code: disabledTemplateSource } } },
224
+ };