@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,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,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
|
+
};
|