@aleph-alpha/chat-kit 1.0.0 → 1.1.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 (118) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-744ZHsQU.js → CkInput.vue_vue_type_script_setup_true_lang-DpYyyCgU.js} +23 -68
  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/index.js +1 -1
  9. package/package.json +5 -5
  10. package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +91 -0
  11. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +215 -0
  12. package/src/components/base/CkAgentMessage/CkAgentMessage.vue +25 -0
  13. package/src/components/base/CkAgentMessage/index.ts +2 -0
  14. package/src/components/base/CkAgentMessage/types.ts +11 -0
  15. package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +129 -0
  16. package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +251 -0
  17. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +206 -0
  18. package/src/components/base/CkConversationLayout/index.ts +2 -0
  19. package/src/components/base/CkConversationLayout/types.ts +18 -0
  20. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts +89 -0
  21. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts +70 -0
  22. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue +110 -0
  23. package/src/components/base/CkCopyMessageActionButton/index.ts +2 -0
  24. package/src/components/base/CkCopyMessageActionButton/types.ts +43 -0
  25. package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +42 -0
  26. package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +230 -0
  27. package/src/components/base/CkHistoryItem/CkHistoryItem.vue +285 -0
  28. package/src/components/base/CkHistoryItem/activeIndicator.ts +15 -0
  29. package/src/components/base/CkHistoryItem/index.ts +2 -0
  30. package/src/components/base/CkHistoryItem/types.ts +111 -0
  31. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.spec.ts +26 -0
  32. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +149 -0
  33. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +36 -0
  34. package/src/components/base/CkHistoryWrapper/index.ts +2 -0
  35. package/src/components/base/CkHistoryWrapper/types.ts +16 -0
  36. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +348 -0
  37. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +382 -0
  38. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +89 -0
  39. package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +19 -0
  40. package/src/components/base/CkMarkdownRenderer/index.ts +5 -0
  41. package/src/components/base/CkMarkdownRenderer/types.ts +53 -0
  42. package/src/components/base/CkMessageActionButton/CkMessageActionButton.spec.ts +40 -0
  43. package/src/components/base/CkMessageActionButton/CkMessageActionButton.stories.ts +56 -0
  44. package/src/components/base/CkMessageActionButton/CkMessageActionButton.vue +23 -0
  45. package/src/components/base/CkMessageActionButton/index.ts +2 -0
  46. package/src/components/base/CkMessageActionButton/types.ts +21 -0
  47. package/src/components/base/CkPromptActionButton/CkPromptActionButton.spec.ts +52 -0
  48. package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +126 -0
  49. package/src/components/base/CkPromptActionButton/CkPromptActionButton.vue +25 -0
  50. package/src/components/base/CkPromptActionButton/index.ts +2 -0
  51. package/src/components/base/CkPromptActionButton/types.ts +32 -0
  52. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +181 -0
  53. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +224 -0
  54. package/src/components/base/CkPromptInput/CkPromptInput.vue +104 -0
  55. package/src/components/base/CkPromptInput/index.ts +2 -0
  56. package/src/components/base/CkPromptInput/types.ts +39 -0
  57. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +40 -0
  58. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +103 -0
  59. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +16 -0
  60. package/src/components/base/CkPromptInputDisclaimer/index.ts +2 -0
  61. package/src/components/base/CkPromptInputDisclaimer/types.ts +17 -0
  62. package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +64 -0
  63. package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +81 -0
  64. package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +27 -0
  65. package/src/components/base/CkPromptStopButton/index.ts +2 -0
  66. package/src/components/base/CkPromptStopButton/types.ts +28 -0
  67. package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +20 -0
  68. package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +112 -0
  69. package/src/components/base/CkUserMessage/CkUserMessage.vue +19 -0
  70. package/src/components/base/CkUserMessage/index.ts +2 -0
  71. package/src/components/base/CkUserMessage/types.ts +14 -0
  72. package/src/components/base/icons/CkSendIcon.vue +22 -0
  73. package/src/components/base/icons/CkStopIcon.vue +18 -0
  74. package/src/components/base/icons/index.ts +2 -0
  75. package/src/components/base/index.ts +21 -0
  76. package/src/components/composed/CkConversation/CkConversation.spec.ts +439 -0
  77. package/src/components/composed/CkConversation/CkConversation.stories.ts +642 -0
  78. package/src/components/composed/CkConversation/CkConversation.vue +146 -0
  79. package/src/components/composed/CkConversation/index.ts +2 -0
  80. package/src/components/composed/CkConversation/types.ts +37 -0
  81. package/src/components/composed/CkHistory/CkHistory.spec.ts +327 -0
  82. package/src/components/composed/CkHistory/CkHistory.stories.ts +373 -0
  83. package/src/components/composed/CkHistory/CkHistory.vue +126 -0
  84. package/src/components/composed/CkHistory/index.ts +2 -0
  85. package/src/components/composed/CkHistory/types.ts +72 -0
  86. package/src/components/composed/CkInput/CkInput.spec.ts +191 -0
  87. package/src/components/composed/CkInput/CkInput.stories.ts +438 -0
  88. package/src/components/composed/CkInput/CkInput.vue +104 -0
  89. package/src/components/composed/CkInput/index.ts +2 -0
  90. package/src/components/composed/CkInput/types.ts +60 -0
  91. package/src/components/composed/index.ts +5 -0
  92. package/src/components/index.ts +34 -0
  93. package/src/composables/index.ts +9 -0
  94. package/src/composables/types.ts +36 -0
  95. package/src/composables/useChat.ts +18 -0
  96. package/src/composables/useConversations.spec.ts +557 -0
  97. package/src/composables/useConversations.ts +321 -0
  98. package/src/handlers/index.ts +9 -0
  99. package/src/handlers/useChatKitLabels.spec.ts +290 -0
  100. package/src/handlers/useChatKitLabels.ts +96 -0
  101. package/src/handlers/useMarkdownParser.spec.ts +112 -0
  102. package/src/handlers/useMarkdownParser.ts +47 -0
  103. package/src/handlers/useMessageHandler.spec.ts +535 -0
  104. package/src/handlers/useMessageHandler.ts +138 -0
  105. package/src/helpers/conversationTree.spec.ts +423 -0
  106. package/src/helpers/conversationTree.ts +117 -0
  107. package/src/helpers/generateId.ts +5 -0
  108. package/src/helpers/index.ts +4 -0
  109. package/src/helpers/secondsToDate.ts +7 -0
  110. package/src/helpers/toError.ts +8 -0
  111. package/src/index.ts +79 -0
  112. package/src/services/ChatService.ts +60 -0
  113. package/src/services/MockChatService.ts +47 -0
  114. package/src/services/index.ts +12 -0
  115. package/src/services/types.ts +92 -0
  116. package/src/test-setup.ts +12 -0
  117. package/src/test-utils/createTextPlugin.ts +9 -0
  118. package/src/types.ts +47 -0
@@ -0,0 +1,191 @@
1
+ import CkInput from './CkInput.vue';
2
+ import { render, screen, fireEvent } from '@testing-library/vue';
3
+
4
+ describe('CkInput', () => {
5
+ describe('defaults', () => {
6
+ it('renders default placeholder', () => {
7
+ render(CkInput);
8
+
9
+ expect(
10
+ screen.getByPlaceholderText('Ask me anything'),
11
+ ).toBeInTheDocument();
12
+ });
13
+
14
+ it('renders default aria label', () => {
15
+ render(CkInput);
16
+
17
+ expect(screen.getByLabelText('Chat message input')).toBeInTheDocument();
18
+ });
19
+
20
+ it('renders default send button', () => {
21
+ render(CkInput);
22
+
23
+ expect(
24
+ screen.getByRole('button', { name: 'Send message' }),
25
+ ).toBeInTheDocument();
26
+ });
27
+ });
28
+
29
+ describe('disclaimer', () => {
30
+ it('renders disclaimer text when provided', () => {
31
+ render(CkInput, {
32
+ props: { disclaimer: 'AI may make mistakes' },
33
+ });
34
+
35
+ expect(screen.getByText('AI may make mistakes')).toBeInTheDocument();
36
+ });
37
+
38
+ it('does not render disclaimer when not provided', () => {
39
+ const { container } = render(CkInput);
40
+
41
+ expect(container.querySelector('p')).not.toBeInTheDocument();
42
+ });
43
+ });
44
+
45
+ describe('v-model', () => {
46
+ it('displays the modelValue in the textarea', () => {
47
+ render(CkInput, { props: { modelValue: 'Hello world' } });
48
+
49
+ expect(screen.getByRole('textbox')).toHaveValue('Hello world');
50
+ });
51
+
52
+ it('emits update:modelValue on input', async () => {
53
+ const { emitted } = render(CkInput);
54
+
55
+ await fireEvent.update(screen.getByRole('textbox'), 'New text');
56
+
57
+ expect(emitted()['update:modelValue']).toBeTruthy();
58
+ expect(emitted()['update:modelValue'][0]).toEqual(['New text']);
59
+ });
60
+ });
61
+
62
+ describe('submit', () => {
63
+ it('emits submit with trimmed text on Enter key', async () => {
64
+ const { emitted } = render(CkInput, {
65
+ props: { modelValue: ' Hello ' },
66
+ });
67
+
68
+ await fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' });
69
+
70
+ expect(emitted().submit).toBeTruthy();
71
+ expect(emitted().submit[0]).toEqual(['Hello']);
72
+ });
73
+
74
+ it('does not emit submit when textarea is empty', async () => {
75
+ const { emitted } = render(CkInput, {
76
+ props: { modelValue: '' },
77
+ });
78
+
79
+ await fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' });
80
+
81
+ expect(emitted().submit).toBeFalsy();
82
+ });
83
+ });
84
+
85
+ describe('loading state', () => {
86
+ it('disables the textarea when loading', () => {
87
+ render(CkInput, { props: { loading: true } });
88
+
89
+ expect(screen.getByRole('textbox')).toBeDisabled();
90
+ });
91
+
92
+ it('does not emit submit when loading', async () => {
93
+ const { emitted } = render(CkInput, {
94
+ props: { modelValue: 'Hello', loading: true },
95
+ });
96
+
97
+ await fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' });
98
+
99
+ expect(emitted().submit).toBeFalsy();
100
+ });
101
+
102
+ it('shows stop control instead of send when loading', () => {
103
+ render(CkInput, { props: { modelValue: 'Hello', loading: true } });
104
+
105
+ expect(
106
+ screen.queryByRole('button', { name: 'Send message' }),
107
+ ).not.toBeInTheDocument();
108
+ expect(
109
+ screen.getByRole('button', { name: 'Stop generating' }),
110
+ ).toBeInTheDocument();
111
+ });
112
+
113
+ it('emits stop when stop control is clicked while loading', async () => {
114
+ const { emitted } = render(CkInput, {
115
+ props: { modelValue: 'Hello', loading: true },
116
+ });
117
+
118
+ await fireEvent.click(
119
+ screen.getByRole('button', { name: 'Stop generating' }),
120
+ );
121
+
122
+ expect(emitted().stop).toBeTruthy();
123
+ });
124
+ });
125
+
126
+ describe('disabled state', () => {
127
+ it('disables the textarea when disabled', () => {
128
+ render(CkInput, { props: { disabled: true } });
129
+
130
+ expect(screen.getByRole('textbox')).toBeDisabled();
131
+ });
132
+
133
+ it('disables the send button when textarea is empty', () => {
134
+ render(CkInput, { props: { modelValue: '' } });
135
+
136
+ expect(
137
+ screen.getByRole('button', { name: 'Send message' }),
138
+ ).toBeDisabled();
139
+ });
140
+
141
+ it('enables the send button when there is text and not disabled', () => {
142
+ render(CkInput, { props: { modelValue: 'Hello' } });
143
+
144
+ expect(
145
+ screen.getByRole('button', { name: 'Send message' }),
146
+ ).toBeEnabled();
147
+ });
148
+ });
149
+
150
+ describe('slots', () => {
151
+ it('renders toolbar-left slot content', () => {
152
+ render(CkInput, {
153
+ slots: { 'toolbar-left': '<button>Express</button>' },
154
+ });
155
+
156
+ expect(screen.getByText('Express')).toBeInTheDocument();
157
+ });
158
+
159
+ it('renders toolbar-right slot content instead of default send button', () => {
160
+ render(CkInput, {
161
+ slots: { 'toolbar-right': '<button>Custom Send</button>' },
162
+ });
163
+
164
+ expect(screen.getByText('Custom Send')).toBeInTheDocument();
165
+ expect(
166
+ screen.queryByRole('button', { name: 'Send message' }),
167
+ ).not.toBeInTheDocument();
168
+ });
169
+
170
+ it('renders footer slot content instead of disclaimer', () => {
171
+ render(CkInput, {
172
+ props: { disclaimer: 'Default disclaimer' },
173
+ slots: { footer: '<span>Custom footer</span>' },
174
+ });
175
+
176
+ expect(screen.getByText('Custom footer')).toBeInTheDocument();
177
+ expect(screen.queryByText('Default disclaimer')).not.toBeInTheDocument();
178
+ });
179
+
180
+ it('renders send-icon slot content inside default send button', () => {
181
+ render(CkInput, {
182
+ slots: { 'send-icon': '<span data-testid="custom-icon">Go</span>' },
183
+ });
184
+
185
+ expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
186
+ expect(
187
+ screen.getByRole('button', { name: 'Send message' }),
188
+ ).toBeInTheDocument();
189
+ });
190
+ });
191
+ });
@@ -0,0 +1,438 @@
1
+ import CkInput from './CkInput.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+ import { ref } from 'vue';
4
+
5
+ const DISCLAIMER =
6
+ 'Assistant Chat may occasionally make mistakes. Please, make sure to double-check important information.';
7
+
8
+ const meta: Meta<typeof CkInput> = {
9
+ title: 'Composed/CkInput',
10
+ component: CkInput,
11
+ tags: ['autodocs'],
12
+ argTypes: {
13
+ modelValue: {
14
+ control: 'text',
15
+ description: 'Current value of the input. Use with `v-model`.',
16
+ },
17
+ placeholder: {
18
+ control: 'text',
19
+ description: 'Placeholder shown when empty.',
20
+ },
21
+ disabled: {
22
+ control: 'boolean',
23
+ description: 'Disables the input and send button.',
24
+ },
25
+ loading: {
26
+ control: 'boolean',
27
+ description:
28
+ 'Shows a spinner in the send button and blocks further input.',
29
+ },
30
+ maxHeight: {
31
+ control: 'number',
32
+ description: 'Max textarea height (px) before it becomes scrollable.',
33
+ },
34
+ ariaLabel: {
35
+ control: 'text',
36
+ description: 'Accessible label for the textarea.',
37
+ },
38
+ disclaimer: {
39
+ control: 'text',
40
+ description: 'Optional disclaimer text rendered below the input.',
41
+ },
42
+ },
43
+ decorators: [
44
+ () => ({
45
+ template:
46
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
47
+ }),
48
+ ],
49
+ };
50
+
51
+ export default meta;
52
+ type Story = StoryObj<typeof meta>;
53
+
54
+ const defaultTemplateSource = `<script setup lang="ts">
55
+ import { CkInput } from '@aleph-alpha/chat-kit';
56
+ import { ref } from 'vue';
57
+
58
+ const text = ref('');
59
+ const disclaimer = 'Assistant Chat may occasionally make mistakes. Please double-check.';
60
+ </script>
61
+
62
+ <template>
63
+ <CkInput v-model="text" :disclaimer="disclaimer" />
64
+ </template>`;
65
+
66
+ const withModeToggleTemplateSource = `<script setup lang="ts">
67
+ import { CkInput } from '@aleph-alpha/chat-kit';
68
+ import { ref } from 'vue';
69
+
70
+ const text = ref('');
71
+ const mode = ref<'express' | 'smart'>('smart');
72
+ </script>
73
+
74
+ <template>
75
+ <CkInput v-model="text">
76
+ <template #toolbar-left>
77
+ <div class="flex items-center gap-1">
78
+ <button @click="mode = 'express'">Express</button>
79
+ <button @click="mode = 'smart'">Smart</button>
80
+ </div>
81
+ </template>
82
+ </CkInput>
83
+ </template>`;
84
+
85
+ const fullExampleTemplateSource = `<script setup lang="ts">
86
+ import { CkInput } from '@aleph-alpha/chat-kit';
87
+ import { ref } from 'vue';
88
+
89
+ const text = ref('');
90
+ const attachmentCount = ref(3);
91
+ const disclaimer = 'Assistant Chat may occasionally make mistakes. Please double-check.';
92
+ function onSubmit() { text.value = ''; }
93
+ </script>
94
+
95
+ <template>
96
+ <CkInput v-model="text" :disclaimer="disclaimer" @submit="onSubmit">
97
+ <template #toolbar-left>
98
+ <button>Mode</button>
99
+ </template>
100
+ <template #toolbar-right="{ submit, disabled }">
101
+ <button aria-label="Attach file">Attach ({{ attachmentCount }})</button>
102
+ <button type="submit" :disabled="disabled" @click="submit" aria-label="Send message">Send</button>
103
+ </template>
104
+ </CkInput>
105
+ </template>`;
106
+
107
+ const disabledTemplateSource = `<script setup lang="ts">
108
+ import { CkInput } from '@aleph-alpha/chat-kit';
109
+ import { ref } from 'vue';
110
+
111
+ const text = ref('');
112
+ </script>
113
+
114
+ <template>
115
+ <CkInput v-model="text" disabled placeholder="Input is disabled" />
116
+ </template>`;
117
+
118
+ const loadingTemplateSource = `<script setup lang="ts">
119
+ import { CkInput } from '@aleph-alpha/chat-kit';
120
+ import { ref } from 'vue';
121
+
122
+ const text = ref('Generating a response...');
123
+ </script>
124
+
125
+ <template>
126
+ <CkInput v-model="text" loading />
127
+ </template>`;
128
+
129
+ const withLongTextTemplateSource = `<script setup lang="ts">
130
+ import { CkInput } from '@aleph-alpha/chat-kit';
131
+ import { ref } from 'vue';
132
+
133
+ const text = ref('This is a long multi-line message that demonstrates auto-grow behavior.');
134
+ </script>
135
+
136
+ <template>
137
+ <CkInput v-model="text" />
138
+ </template>`;
139
+
140
+ const mobileWidthTemplateSource = `<script setup lang="ts">
141
+ import { CkInput } from '@aleph-alpha/chat-kit';
142
+ import { ref } from 'vue';
143
+
144
+ const text = ref('');
145
+ </script>
146
+
147
+ <template>
148
+ <div style="max-width: 320px;">
149
+ <CkInput v-model="text" />
150
+ </div>
151
+ </template>`;
152
+
153
+ /**
154
+ * Default chat input with disclaimer and the built-in send button.
155
+ * Use as the primary way to let users submit a prompt to the assistant.
156
+ */
157
+ export const Default: Story = {
158
+ render: () => ({
159
+ components: { CkInput },
160
+ setup() {
161
+ const text = ref('');
162
+ return { text, DISCLAIMER };
163
+ },
164
+ template: `
165
+ <CkInput
166
+ v-model="text"
167
+ :disclaimer="DISCLAIMER"
168
+ />
169
+ `,
170
+ }),
171
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
172
+ };
173
+
174
+ /**
175
+ * Adds a mode toggle (Express/Smart) to the left side of the toolbar via
176
+ * `#toolbar-left`. Use when the user needs to switch between assistant
177
+ * presets or capabilities before submitting.
178
+ */
179
+ export const WithModeToggle: Story = {
180
+ render: () => ({
181
+ components: { CkInput },
182
+ setup() {
183
+ const text = ref('');
184
+ const mode = ref<'express' | 'smart'>('smart');
185
+ return { text, mode };
186
+ },
187
+ template: `
188
+ <CkInput v-model="text">
189
+ <template #toolbar-left>
190
+ <div class="flex items-center">
191
+ <button
192
+ class="flex items-center justify-center rounded-l-md border border-solid h-9 px-2 text-sm font-medium shadow-sm transition-all duration-300 ease-in-out"
193
+ :style="mode === 'express'
194
+ ? 'background-color: #2888b8; border-color: rgba(183,180,164,0.3); color: #daf2f6;'
195
+ : 'background-color: #ebebe5; border-color: rgba(183,180,164,0.3); color: #1d1d1b;'"
196
+ @click="mode = 'express'"
197
+ >
198
+ <svg class="shrink-0" 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="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg>
199
+ <span
200
+ class="overflow-hidden whitespace-nowrap transition-all duration-300 ease-in-out"
201
+ :style="mode === 'express'
202
+ ? 'max-width: 70px; opacity: 1; padding-left: 8px;'
203
+ : 'max-width: 0; opacity: 0; padding-left: 0;'"
204
+ >Express</span>
205
+ </button>
206
+ <button
207
+ class="flex items-center justify-center rounded-r-md border-t border-r border-b border-solid h-9 px-2 text-sm font-medium shadow-sm transition-all duration-300 ease-in-out"
208
+ :style="mode === 'smart'
209
+ ? 'background-color: #2888b8; border-color: rgba(183,180,164,0.3); color: #daf2f6;'
210
+ : 'background-color: #ebebe5; border-color: rgba(183,180,164,0.3); color: #1d1d1b;'"
211
+ @click="mode = 'smart'"
212
+ >
213
+ <svg class="shrink-0" 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="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>
214
+ <span
215
+ class="overflow-hidden whitespace-nowrap transition-all duration-300 ease-in-out"
216
+ :style="mode === 'smart'
217
+ ? 'max-width: 70px; opacity: 1; padding-left: 8px;'
218
+ : 'max-width: 0; opacity: 0; padding-left: 0;'"
219
+ >Smart</span>
220
+ </button>
221
+ </div>
222
+ </template>
223
+ </CkInput>
224
+ `,
225
+ }),
226
+ parameters: { docs: { source: { code: withModeToggleTemplateSource } } },
227
+ };
228
+
229
+ /**
230
+ * End-to-end example: mode toggle on the left, attachment button + custom
231
+ * send button on the right, and a disclaimer underneath. Use as a reference
232
+ * for a production-ready chat compose UI.
233
+ */
234
+ export const FullExample: Story = {
235
+ render: () => ({
236
+ components: { CkInput },
237
+ setup() {
238
+ const text = ref('');
239
+ const mode = ref<'express' | 'smart'>('smart');
240
+ const attachmentCount = ref(3);
241
+ const onSubmit = () => {
242
+ text.value = '';
243
+ };
244
+ return { text, mode, attachmentCount, onSubmit, DISCLAIMER };
245
+ },
246
+ template: `
247
+ <CkInput
248
+ v-model="text"
249
+ :disclaimer="DISCLAIMER"
250
+ @submit="onSubmit"
251
+ >
252
+ <template #toolbar-left>
253
+ <div class="flex items-center">
254
+ <button
255
+ class="flex items-center justify-center rounded-l-md border border-solid h-9 px-2 text-sm font-medium shadow-sm transition-all duration-300 ease-in-out"
256
+ :style="mode === 'express'
257
+ ? 'background-color: #2888b8; border-color: rgba(183,180,164,0.3); color: #daf2f6;'
258
+ : 'background-color: #ebebe5; border-color: rgba(183,180,164,0.3); color: #1d1d1b;'"
259
+ @click="mode = 'express'"
260
+ >
261
+ <svg class="shrink-0" 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="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg>
262
+ <span
263
+ class="overflow-hidden whitespace-nowrap transition-all duration-300 ease-in-out"
264
+ :style="mode === 'express'
265
+ ? 'max-width: 70px; opacity: 1; padding-left: 8px;'
266
+ : 'max-width: 0; opacity: 0; padding-left: 0;'"
267
+ >Express</span>
268
+ </button>
269
+ <button
270
+ class="flex items-center justify-center rounded-r-md border-t border-r border-b border-solid h-9 px-2 text-sm font-medium shadow-sm transition-all duration-300 ease-in-out"
271
+ :style="mode === 'smart'
272
+ ? 'background-color: #2888b8; border-color: rgba(183,180,164,0.3); color: #daf2f6;'
273
+ : 'background-color: #ebebe5; border-color: rgba(183,180,164,0.3); color: #1d1d1b;'"
274
+ @click="mode = 'smart'"
275
+ >
276
+ <svg class="shrink-0" 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="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>
277
+ <span
278
+ class="overflow-hidden whitespace-nowrap transition-all duration-300 ease-in-out"
279
+ :style="mode === 'smart'
280
+ ? 'max-width: 70px; opacity: 1; padding-left: 8px;'
281
+ : 'max-width: 0; opacity: 0; padding-left: 0;'"
282
+ >Smart</span>
283
+ </button>
284
+ </div>
285
+ </template>
286
+ <template #toolbar-right="{ submit, disabled }">
287
+ <div class="flex items-center gap-2">
288
+ <button
289
+ type="button"
290
+ class="flex items-center gap-2 rounded-md border border-solid px-4 py-2 h-9"
291
+ style="border-color: rgba(183, 180, 164, 0.3);"
292
+ aria-label="Attach file"
293
+ >
294
+ <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="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
295
+ <span
296
+ v-if="attachmentCount > 0"
297
+ class="flex items-center justify-center rounded-full min-w-5 h-4 px-1 text-xs"
298
+ style="background-color: #c9e400; color: #252422;"
299
+ >
300
+ {{ attachmentCount }}
301
+ </span>
302
+ </button>
303
+ <button
304
+ type="submit"
305
+ :disabled="disabled"
306
+ class="flex items-center justify-center size-9 rounded-md disabled:opacity-50"
307
+ style="background-color: #c9e400; color: #1d1d1b;"
308
+ aria-label="Send message"
309
+ @click="submit"
310
+ >
311
+ <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>
312
+ </button>
313
+ </div>
314
+ </template>
315
+ </CkInput>
316
+ `,
317
+ }),
318
+ decorators: [
319
+ () => ({
320
+ template:
321
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px; background: #ebebe5; min-height: 300px; border-radius: 12px;"><story /></div>',
322
+ }),
323
+ ],
324
+ parameters: { docs: { source: { code: fullExampleTemplateSource } } },
325
+ };
326
+
327
+ /**
328
+ * Disabled state — the textarea is read-only and the send button is inactive.
329
+ * Use while waiting for an upstream action to resolve.
330
+ */
331
+ export const Disabled: Story = {
332
+ render: () => ({
333
+ components: { CkInput },
334
+ setup() {
335
+ const text = ref('');
336
+ return { text, DISCLAIMER };
337
+ },
338
+ template: `
339
+ <CkInput
340
+ v-model="text"
341
+ disabled
342
+ placeholder="Input is disabled"
343
+ :disclaimer="DISCLAIMER"
344
+ />
345
+ `,
346
+ }),
347
+ parameters: { docs: { source: { code: disabledTemplateSource } } },
348
+ };
349
+
350
+ /**
351
+ * Loading state — shown while the assistant is generating a reply. The
352
+ * send button displays a spinner and the input is blocked.
353
+ */
354
+ export const Loading: Story = {
355
+ render: () => ({
356
+ components: { CkInput },
357
+ setup() {
358
+ const text = ref('Generating a response...');
359
+ const loading = ref(true);
360
+ function onSubmit() {
361
+ text.value = 'Generating a response...';
362
+ loading.value = true;
363
+ }
364
+ function onStop() {
365
+ text.value = 'Response stopped';
366
+ loading.value = false;
367
+ }
368
+ return { text, loading, DISCLAIMER, onSubmit, onStop };
369
+ },
370
+ template: `
371
+ <CkInput
372
+ v-model="text"
373
+ :loading="loading"
374
+ :disclaimer="DISCLAIMER"
375
+ @submit="onSubmit"
376
+ @stop="onStop"
377
+ />
378
+ `,
379
+ }),
380
+ parameters: { docs: { source: { code: loadingTemplateSource } } },
381
+ };
382
+
383
+ /**
384
+ * Demonstrates the textarea's auto-grow behavior with multi-paragraph content.
385
+ * Use when validating how the input handles long prompts.
386
+ */
387
+ export const WithLongText: Story = {
388
+ render: () => ({
389
+ components: { CkInput },
390
+ setup() {
391
+ const text = ref(
392
+ 'This is a long message that demonstrates the auto-grow behavior of the textarea. ' +
393
+ 'As the content grows, the textarea expands up to the maximum height. ' +
394
+ 'Once the max height is reached, the content becomes scrollable.\n\n' +
395
+ 'Here is a second paragraph to show how multi-line content works. ' +
396
+ 'The component handles newlines naturally with Shift+Enter.\n\n' +
397
+ 'And a third paragraph for good measure. The auto-grow logic recalculates ' +
398
+ 'the height on every input event and when the modelValue changes externally.',
399
+ );
400
+ return { text, DISCLAIMER };
401
+ },
402
+ template: `
403
+ <CkInput
404
+ v-model="text"
405
+ :disclaimer="DISCLAIMER"
406
+ />
407
+ `,
408
+ }),
409
+ parameters: { docs: { source: { code: withLongTextTemplateSource } } },
410
+ };
411
+
412
+ /**
413
+ * Narrow viewport preview (≈320px). Use to verify the input stays usable on
414
+ * small screens.
415
+ */
416
+ export const MobileWidth: Story = {
417
+ render: () => ({
418
+ components: { CkInput },
419
+ setup() {
420
+ const text = ref('');
421
+ return { text, DISCLAIMER };
422
+ },
423
+ template: `
424
+ <div style="max-width: 320px;">
425
+ <CkInput
426
+ v-model="text"
427
+ :disclaimer="DISCLAIMER"
428
+ />
429
+ </div>
430
+ `,
431
+ }),
432
+ decorators: [
433
+ () => ({
434
+ template: '<div style="padding: 16px;"><story /></div>',
435
+ }),
436
+ ],
437
+ parameters: { docs: { source: { code: mobileWidthTemplateSource } } },
438
+ };