@aleph-alpha/chat-kit 4.0.0 → 5.0.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 (110) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-DJN-nZ2-.js → CkHistory.vue_vue_type_script_setup_true_lang-Bp9hXdH6.js} +434 -377
  2. package/dist/chat-kit.css +28 -6
  3. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +14 -13
  4. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
  5. package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts +8 -7
  6. package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts.map +1 -1
  7. package/dist/components/base/CkAgentMessage/types.d.ts +16 -5
  8. package/dist/components/base/CkAgentMessage/types.d.ts.map +1 -1
  9. package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.d.ts +16 -0
  10. package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.d.ts.map +1 -0
  11. package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue.d.ts +21 -0
  12. package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue.d.ts.map +1 -0
  13. package/dist/components/base/CkAgentMessageHeader/index.d.ts +3 -0
  14. package/dist/components/base/CkAgentMessageHeader/index.d.ts.map +1 -0
  15. package/dist/components/base/CkAgentMessageHeader/types.d.ts +22 -0
  16. package/dist/components/base/CkAgentMessageHeader/types.d.ts.map +1 -0
  17. package/dist/components/base/CkCitation/CkCitation.stories.d.ts +6 -0
  18. package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -1
  19. package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
  20. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +5 -4
  21. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -1
  22. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -1
  23. package/dist/components/base/CkFileItemsList/CkFileItemsList.stories.d.ts +15 -4
  24. package/dist/components/base/CkFileItemsList/CkFileItemsList.stories.d.ts.map +1 -1
  25. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +2 -1
  26. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts.map +1 -1
  27. package/dist/components/base/CkFileItemsList/types.d.ts +17 -4
  28. package/dist/components/base/CkFileItemsList/types.d.ts.map +1 -1
  29. package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.d.ts +19 -0
  30. package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.d.ts.map +1 -0
  31. package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue.d.ts +22 -0
  32. package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue.d.ts.map +1 -0
  33. package/dist/components/base/CkHeaderActivityButton/index.d.ts +3 -0
  34. package/dist/components/base/CkHeaderActivityButton/index.d.ts.map +1 -0
  35. package/dist/components/base/CkHeaderActivityButton/types.d.ts +36 -0
  36. package/dist/components/base/CkHeaderActivityButton/types.d.ts.map +1 -0
  37. package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts +6 -6
  38. package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts.map +1 -1
  39. package/dist/components/base/CkPromptInput/types.d.ts +4 -4
  40. package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue.d.ts.map +1 -1
  41. package/dist/components/base/CkPromptInputDisclaimer/types.d.ts +1 -1
  42. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
  43. package/dist/components/base/CkPromptSendButton/types.d.ts +1 -1
  44. package/dist/components/base/CkPromptStopButton/types.d.ts +1 -1
  45. package/dist/components/base/index.d.ts +4 -0
  46. package/dist/components/base/index.d.ts.map +1 -1
  47. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +13 -1
  48. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
  49. package/dist/components/composed/CkConversation/types.d.ts +1 -1
  50. package/dist/components/composed/index.d.ts +0 -1
  51. package/dist/components/composed/index.d.ts.map +1 -1
  52. package/dist/components/index.d.ts +3 -3
  53. package/dist/components/index.d.ts.map +1 -1
  54. package/dist/components/index.js +26 -25
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +27 -26
  58. package/package.json +1 -1
  59. package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +12 -17
  60. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +124 -113
  61. package/src/components/base/CkAgentMessage/CkAgentMessage.vue +18 -16
  62. package/src/components/base/CkAgentMessage/types.ts +16 -5
  63. package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.spec.ts +64 -0
  64. package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.ts +90 -0
  65. package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue +47 -0
  66. package/src/components/base/CkAgentMessageHeader/index.ts +2 -0
  67. package/src/components/base/CkAgentMessageHeader/types.ts +21 -0
  68. package/src/components/base/CkCitation/CkCitation.stories.ts +12 -0
  69. package/src/components/base/CkCitation/CkCitation.vue +34 -9
  70. package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +5 -4
  71. package/src/components/base/CkCitationCard/CkCitationCard.vue +2 -2
  72. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +1 -1
  73. package/src/components/base/CkFileItemsList/CkFileItemsList.spec.ts +88 -13
  74. package/src/components/base/CkFileItemsList/CkFileItemsList.stories.ts +61 -13
  75. package/src/components/base/CkFileItemsList/CkFileItemsList.vue +153 -110
  76. package/src/components/base/CkFileItemsList/types.ts +17 -4
  77. package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.spec.ts +50 -0
  78. package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.ts +102 -0
  79. package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue +39 -0
  80. package/src/components/base/CkHeaderActivityButton/index.ts +2 -0
  81. package/src/components/base/CkHeaderActivityButton/types.ts +36 -0
  82. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +88 -0
  83. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +6 -6
  84. package/src/components/base/CkPromptInput/CkPromptInput.vue +63 -12
  85. package/src/components/base/CkPromptInput/types.ts +4 -4
  86. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +1 -1
  87. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +1 -1
  88. package/src/components/base/CkPromptInputDisclaimer/types.ts +1 -1
  89. package/src/components/base/CkPromptSendButton/types.ts +1 -1
  90. package/src/components/base/CkPromptStopButton/types.ts +1 -1
  91. package/src/components/base/index.ts +7 -0
  92. package/src/components/composed/CkConversation/CkConversation.spec.ts +81 -6
  93. package/src/components/composed/CkConversation/CkConversation.vue +78 -35
  94. package/src/components/composed/CkConversation/types.ts +1 -1
  95. package/src/components/composed/index.ts +0 -1
  96. package/src/components/index.ts +6 -1
  97. package/src/index.ts +5 -1
  98. package/dist/components/composed/CkInput/CkInput.stories.d.ts +0 -43
  99. package/dist/components/composed/CkInput/CkInput.stories.d.ts.map +0 -1
  100. package/dist/components/composed/CkInput/CkInput.vue.d.ts +0 -50
  101. package/dist/components/composed/CkInput/CkInput.vue.d.ts.map +0 -1
  102. package/dist/components/composed/CkInput/index.d.ts +0 -3
  103. package/dist/components/composed/CkInput/index.d.ts.map +0 -1
  104. package/dist/components/composed/CkInput/types.d.ts +0 -55
  105. package/dist/components/composed/CkInput/types.d.ts.map +0 -1
  106. package/src/components/composed/CkInput/CkInput.spec.ts +0 -191
  107. package/src/components/composed/CkInput/CkInput.stories.ts +0 -434
  108. package/src/components/composed/CkInput/CkInput.vue +0 -106
  109. package/src/components/composed/CkInput/index.ts +0 -2
  110. package/src/components/composed/CkInput/types.ts +0 -55
@@ -657,8 +657,8 @@ function onSubmit(value: string): void {
657
657
  * End-to-end composition matching the Figma "Prompt field" spec: file
658
658
  * attachments via the `+` button and drag-and-drop, model selection next to
659
659
  * the send button, and a hallucination disclaimer in the footer. File chips
660
- * collapse to a popover once more than three are attached (`CkFileItemsList`
661
- * default).
660
+ * collapse to a popover once they no longer fit the prompt field's width
661
+ * (`CkFileItemsList` keeps each chip at least `minItemWidth` wide).
662
662
  */
663
663
  export const WithUploadAndModelSelection: Story = {
664
664
  render: () => ({
@@ -955,10 +955,10 @@ function removeFile(id: CkFileEntry['id']) {
955
955
  </template>`;
956
956
 
957
957
  /**
958
- * Five attached files — past the default `maxInline=3` threshold of
959
- * `CkFileItemsList`, so the list collapses to a single popover trigger
960
- * labeled "5 files". Click it to inspect / remove the individual items.
961
- * The toolbar-left `+` menu stays available so users can keep adding more.
958
+ * Five attached files — more than fit inline at the prompt field's width, so
959
+ * `CkFileItemsList` collapses to a single popover trigger labeled "5 files".
960
+ * Click it to inspect / remove the individual items. The toolbar-left `+` menu
961
+ * stays available so users can keep adding more.
962
962
  */
963
963
  export const WithCollapsedFileChips: Story = {
964
964
  render: () => ({
@@ -14,13 +14,8 @@ import {
14
14
 
15
15
  defineOptions({ name: 'CkPromptInput' });
16
16
 
17
- // Pixel height of one rendered line — must match the textarea's `leading-6`
18
- // (24 px). Used to derive how many lines the current content occupies.
19
- const LINE_HEIGHT = 24;
20
-
21
17
  // Cap the textarea at 6 visible lines; content beyond that scrolls inside.
22
18
  const MAX_LINES = 6;
23
- const MAX_HEIGHT = LINE_HEIGHT * MAX_LINES;
24
19
 
25
20
  const props = withDefaults(defineProps<CkPromptInputProps>(), {
26
21
  modelValue: '',
@@ -84,19 +79,25 @@ function adjustHeight(): void {
84
79
  return;
85
80
  }
86
81
 
82
+ // Line height is fluid (it scales with the container via `clamp()`), so read
83
+ // the resolved px value from the DOM instead of a hard-coded constant — the
84
+ // 6-line cap and the 2-line latch threshold both depend on it.
85
+ const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || 24;
86
+ const maxHeight = lineHeight * MAX_LINES;
87
+
87
88
  // Collapse to natural content height first so the next scrollHeight read
88
89
  // reflects what the content needs, not what the inline height is forcing.
89
90
  el.style.height = 'auto';
90
91
  const measured = el.scrollHeight;
91
- el.style.height = `${Math.min(measured, MAX_HEIGHT)}px`;
92
+ el.style.height = `${Math.min(measured, maxHeight)}px`;
92
93
 
93
94
  // Latch only on a *real* second line: either explicit newline OR scrollHeight
94
- // ≥ 2 × line-height. Using `Math.round` on a borderline 36 px (sub-pixel
95
- // emoji/CJK rendering on Chromium/Linux) would falsely latch a single line.
95
+ // ≥ 2 × line-height. Using `Math.round` on a borderline single line (sub-pixel
96
+ // emoji/CJK rendering on Chromium/Linux) would falsely latch it.
96
97
  const hasExplicitNewline = props.modelValue.includes('\n');
97
98
  if (
98
99
  !isMultiLine.value &&
99
- (hasExplicitNewline || measured >= LINE_HEIGHT * 2)
100
+ (hasExplicitNewline || measured >= lineHeight * 2)
100
101
  ) {
101
102
  isMultiLine.value = true;
102
103
  }
@@ -124,6 +125,28 @@ function focus(): void {
124
125
  textareaRef.value?.focus();
125
126
  }
126
127
 
128
+ // Highlight (border + ring) should appear for keyboard navigation only, not
129
+ // mouse clicks. `:focus-visible` can't express this on a textarea — browsers
130
+ // always treat text fields as focus-visible since they take keyboard input —
131
+ // so we track the focus origin ourselves: a pointerdown immediately preceding
132
+ // a focus means the mouse caused it, so suppress the highlight.
133
+ const isKeyboardFocused = ref(false);
134
+ let focusedByPointer = false;
135
+
136
+ function onPointerDown(): void {
137
+ focusedByPointer = true;
138
+ }
139
+
140
+ function onFocus(): void {
141
+ isKeyboardFocused.value = !focusedByPointer;
142
+ focusedByPointer = false;
143
+ }
144
+
145
+ function onBlur(): void {
146
+ isKeyboardFocused.value = false;
147
+ focusedByPointer = false;
148
+ }
149
+
127
150
  watch(
128
151
  () => props.modelValue,
129
152
  () => nextTick(adjustHeight),
@@ -134,11 +157,14 @@ defineExpose({ focus, submit });
134
157
  </script>
135
158
 
136
159
  <template>
137
- <div class="flex flex-col gap-2">
160
+ <div class="ck-prompt-root flex flex-col gap-2">
138
161
  <form
139
162
  :class="[
140
- 'ck-prompt-form border-border-surface-default bg-background-input-strong [&:has(textarea:focus)]:border-border-surface-focus-ring [&:has(textarea:focus)]:ring-border-surface-focus-ring/40 min-h-17 grid items-center gap-x-0 gap-y-5 border p-4 transition-[border-radius] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] [&:has(textarea:focus)]:ring-2',
163
+ 'border-border-surface-default bg-background-input-strong min-h-17 grid items-center gap-x-0 gap-y-5 border p-4 transition-[border-radius] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] ',
141
164
  hasMultipleRows ? 'rounded-2xl' : 'rounded-3xl',
165
+ isKeyboardFocused
166
+ ? 'border-border-surface-focus-ring ring-border-surface-focus-ring/40 ring-2'
167
+ : 'ck-prompt-form',
142
168
  ]"
143
169
  :style="{
144
170
  gridTemplateColumns: 'auto 1fr auto',
@@ -164,11 +190,14 @@ defineExpose({ focus, submit });
164
190
  :disabled="disabled"
165
191
  style="grid-area: input"
166
192
  :class="[
167
- 'ck-prompt-textarea text-content-on-surface-primary placeholder-content-on-surface-muted w-full min-w-0 resize-none overflow-y-auto bg-transparent py-0 font-sans text-base font-normal leading-6 max-h-36 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60',
193
+ 'ck-prompt-textarea text-content-on-surface-primary placeholder-content-on-surface-muted w-full min-w-0 resize-none overflow-y-auto bg-transparent py-0 font-sans font-normal focus:outline-none disabled:cursor-not-allowed disabled:opacity-60',
168
194
  isMultiLine ? 'pl-3 pr-3' : 'px-0',
169
195
  ]"
170
196
  @input="onInput"
171
197
  @keydown="onKeydown"
198
+ @pointerdown="onPointerDown"
199
+ @focus="onFocus"
200
+ @blur="onBlur"
172
201
  />
173
202
  <div :class="[!isMultiLine && 'pl-4']" style="grid-area: right">
174
203
  <slot
@@ -184,15 +213,37 @@ defineExpose({ focus, submit });
184
213
  </template>
185
214
 
186
215
  <style scoped>
216
+ /* Fluid mobile↔desktop sizing. Sizes interpolate against the component's own
217
+ container width (`cqw`), gradually transforming between the Figma Mobile
218
+ variant (≤320px container) and the Desktop variant (≥768px container) rather
219
+ than snapping at a breakpoint. Range = 768 − 320 = 448; each term is
220
+ `clamp(mobile, calc(mobile + (100cqw − 320px) × span / 448), desktop)`. */
221
+ .ck-prompt-root {
222
+ container-type: inline-size;
223
+ }
224
+
187
225
  /* Solea two-layer drop shadow — warm grey + subtle orange tint. Matches the
188
226
  `shadow/md/{1,2}` tokens from the design system; the generic `shadow-md`
189
227
  utility from preset-wind4 uses a cold black-based shadow instead. */
190
228
  .ck-prompt-form {
229
+ /* 8px (mobile) → 16px (desktop) */
230
+ padding: clamp(8px, calc(8px + (100cqw - 320px) * 8 / 448), 16px);
231
+ /* 52px → 68px (= 36px button + 2× padding) */
232
+ min-height: clamp(52px, calc(52px + (100cqw - 320px) * 16 / 448), 68px);
191
233
  box-shadow:
192
234
  0 4px 6px -2px rgba(112, 95, 90, 0.1),
193
235
  0 6px 14px -2px rgba(248, 94, 43, 0.05);
194
236
  }
195
237
 
238
+ /* 14px → 16px font, 20px → 24px line-height, 120px → 144px (6 lines) cap.
239
+ `adjustHeight` reads the resolved line-height from the DOM so the auto-grow
240
+ math tracks these fluid values. */
241
+ .ck-prompt-textarea {
242
+ font-size: clamp(14px, calc(14px + (100cqw - 320px) * 2 / 448), 16px);
243
+ line-height: clamp(20px, calc(20px + (100cqw - 320px) * 4 / 448), 24px);
244
+ max-height: clamp(120px, calc(120px + (100cqw - 320px) * 24 / 448), 144px);
245
+ }
246
+
196
247
  /* Slim styled scrollbar for the multi-line textarea — matches Figma's 4 px
197
248
  muted-default track. Firefox uses the native `scrollbar-*` properties;
198
249
  Chromium/WebKit needs the `::-webkit-scrollbar` pseudo-elements. */
@@ -4,14 +4,14 @@
4
4
  * slots and keyboard submission (Enter to submit, Shift+Enter for newline).
5
5
  * The form layout adapts between a compact single-row variant (large 3xl
6
6
  * radius) and a stacked multi-row variant (2xl radius) automatically — driven
7
- * by content height and whether `toolbar-top` is filled. Use when you need
8
- * full control over the prompt layout; for a turnkey chat input use `CkInput`
9
- * instead.
7
+ * by content height and whether `toolbar-top` is filled. Compose it with
8
+ * `CkPromptSendButton`, `CkPromptStopButton`, and `CkPromptInputDisclaimer`
9
+ * to build a complete chat input.
10
10
  *
11
11
  * @category Form Inputs
12
12
  * @useCases raw chat prompt textarea, custom prompt toolbar, auto-growing compose box, file-attachment chip row, multi-line chat input, keyboard submit
13
13
  * @keywords prompt, input, textarea, chat, compose, submit, v-model, auto-grow, multi-line, toolbar
14
- * @related CkInput, CkPromptSendButton, CkPromptStopButton, CkPromptMenuButton, CkPromptInputDisclaimer, CkFileItemsList
14
+ * @related CkPromptSendButton, CkPromptStopButton, CkPromptMenuButton, CkPromptInputDisclaimer, CkFileItemsList
15
15
  */
16
16
  export interface CkPromptInputProps {
17
17
  /**
@@ -33,7 +33,7 @@ describe('CkPromptInputDisclaimer', () => {
33
33
 
34
34
  const el = screen.getByText('Disclaimer');
35
35
  expect(el.tagName).toBe('P');
36
- expect(el.className).toContain('text-core-content-tertiary');
36
+ expect(el.className).toContain('text-content-on-surface-muted');
37
37
  expect(el.className).toContain('text-center');
38
38
  expect(el.className).toContain('text-xs');
39
39
  });
@@ -9,7 +9,7 @@ defineProps<CkPromptInputDisclaimerProps>();
9
9
  <template>
10
10
  <p
11
11
  v-if="$slots.default || text"
12
- class="text-core-content-tertiary text-center text-xs leading-3.5"
12
+ class="text-center text-content-on-surface-muted text-xs font-normal leading-4"
13
13
  >
14
14
  <slot>{{ text }}</slot>
15
15
  </p>
@@ -6,7 +6,7 @@
6
6
  * @category Feedback
7
7
  * @useCases chat disclaimer, ai may make mistakes, prompt footnote, small help text under input
8
8
  * @keywords disclaimer, notice, footer, help text, ai, prompt
9
- * @related CkInput, CkPromptInput
9
+ * @related CkPromptInput, CkPromptSendButton
10
10
  */
11
11
  export interface CkPromptInputDisclaimerProps {
12
12
  /**
@@ -7,7 +7,7 @@
7
7
  * @category Actions
8
8
  * @useCases send message button, submit prompt, primary chat send action, loading send button, disabled send button
9
9
  * @keywords prompt, send, submit, button, icon, loading, brand
10
- * @related CkPromptInput, CkPromptStopButton, CkInput, CkSendIcon
10
+ * @related CkPromptInput, CkPromptStopButton, CkSendIcon
11
11
  */
12
12
  export interface CkPromptSendButtonProps {
13
13
  /**
@@ -8,7 +8,7 @@
8
8
  * @category Actions
9
9
  * @useCases stop assistant generation, cancel streaming reply, abort in-flight request, halt llm response
10
10
  * @keywords stop, cancel, abort, prompt, button, icon, streaming, secondary
11
- * @related CkPromptSendButton, CkPromptMenuButton, CkPromptInput, CkInput, CkStopIcon
11
+ * @related CkPromptSendButton, CkPromptMenuButton, CkPromptInput, CkStopIcon
12
12
  */
13
13
  export interface CkPromptStopButtonProps {
14
14
  /**
@@ -1,5 +1,12 @@
1
1
  export { CkSendIcon, CkStopIcon } from './icons';
2
2
  export { CkAgentMessage } from './CkAgentMessage';
3
+ export { CkAgentMessageHeader } from './CkAgentMessageHeader';
4
+ export type { CkAgentMessageHeaderProps } from './CkAgentMessageHeader';
5
+ export { CkHeaderActivityButton } from './CkHeaderActivityButton';
6
+ export type {
7
+ CkHeaderActivityButtonProps,
8
+ CkHeaderActivityButtonEmits,
9
+ } from './CkHeaderActivityButton';
3
10
  export { CkCitation } from './CkCitation';
4
11
  export type {
5
12
  CkCitationProps,
@@ -137,7 +137,7 @@ describe('CkConversation', () => {
137
137
  });
138
138
 
139
139
  expect(
140
- screen.queryByTestId('ck-agent-message-icon'),
140
+ screen.queryByTestId('ck-agent-message-header-icon'),
141
141
  ).not.toBeInTheDocument();
142
142
  });
143
143
 
@@ -146,7 +146,9 @@ describe('CkConversation', () => {
146
146
  props: { messages: [agentMessage()], agentAvatarFallback: 'AI' },
147
147
  });
148
148
 
149
- expect(screen.getByTestId('ck-agent-message-icon')).toBeInTheDocument();
149
+ expect(
150
+ screen.getByTestId('ck-agent-message-header-icon'),
151
+ ).toBeInTheDocument();
150
152
  });
151
153
 
152
154
  it('shows the icon area when agentAvatarImage URL is provided', () => {
@@ -157,7 +159,9 @@ describe('CkConversation', () => {
157
159
  },
158
160
  });
159
161
 
160
- expect(screen.getByTestId('ck-agent-message-icon')).toBeInTheDocument();
162
+ expect(
163
+ screen.getByTestId('ck-agent-message-header-icon'),
164
+ ).toBeInTheDocument();
161
165
  });
162
166
 
163
167
  it('shows the icon area when agent-message-icon slot is provided', () => {
@@ -169,7 +173,9 @@ describe('CkConversation', () => {
169
173
  });
170
174
 
171
175
  expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
172
- expect(screen.getByTestId('ck-agent-message-icon')).toBeInTheDocument();
176
+ expect(
177
+ screen.getByTestId('ck-agent-message-header-icon'),
178
+ ).toBeInTheDocument();
173
179
  });
174
180
  });
175
181
 
@@ -360,15 +366,44 @@ describe('CkConversation', () => {
360
366
  expect(screen.getByAltText('custom-bot')).toBeInTheDocument();
361
367
  });
362
368
 
363
- it('allows adding a header via agent-message-header slot', () => {
369
+ it('allows replacing the entire header via agent-message-header slot', () => {
370
+ render(CkConversation, {
371
+ // An avatar fallback would normally render the default header icon...
372
+ props: { messages: [agentMessage()], agentAvatarFallback: 'AI' },
373
+ slots: {
374
+ 'agent-message-header':
375
+ '<div data-testid="custom-header">Custom Header</div>',
376
+ },
377
+ });
378
+
379
+ expect(screen.getByTestId('custom-header')).toBeInTheDocument();
380
+ // ...but the whole default header (and its icon) is replaced.
381
+ expect(
382
+ screen.queryByTestId('ck-agent-message-header-icon'),
383
+ ).not.toBeInTheDocument();
384
+ });
385
+
386
+ it('allows setting the header text via agent-message-header-content slot', () => {
364
387
  render(CkConversation, {
365
388
  props: { messages: [agentMessage()] },
366
- slots: { 'agent-message-header': 'Custom Header' },
389
+ slots: { 'agent-message-header-content': 'Custom Header' },
367
390
  });
368
391
 
369
392
  expect(screen.getByText('Custom Header')).toBeInTheDocument();
370
393
  });
371
394
 
395
+ it('allows adding a header activity control via agent-message-header-activity slot', () => {
396
+ render(CkConversation, {
397
+ props: { messages: [agentMessage()] },
398
+ slots: {
399
+ 'agent-message-header-activity':
400
+ '<button data-testid="activity">1 tool</button>',
401
+ },
402
+ });
403
+
404
+ expect(screen.getByTestId('activity')).toBeInTheDocument();
405
+ });
406
+
372
407
  it('allows adding footer actions via agent-message-footer slot', () => {
373
408
  render(CkConversation, {
374
409
  props: { messages: [agentMessage()] },
@@ -429,6 +464,46 @@ describe('CkConversation', () => {
429
464
  });
430
465
  });
431
466
 
467
+ describe('bottom separator', () => {
468
+ function agentRowBorders(container: Element): boolean[] {
469
+ const rows = container.querySelectorAll<HTMLElement>(
470
+ '[data-message-row="assistant"]',
471
+ );
472
+ return Array.from(rows).map((row) => {
473
+ const message = row.querySelector<HTMLElement>('[class*="pb-6"]');
474
+ return Boolean(message?.className.includes('border-b'));
475
+ });
476
+ }
477
+
478
+ it('keeps the separator on every assistant message except the last', () => {
479
+ const { container } = render(CkConversation, {
480
+ props: {
481
+ messages: [
482
+ { ...agentMessage(), id: 'a1' },
483
+ {
484
+ id: 'u1',
485
+ role: 'user',
486
+ content: [text('Hi')],
487
+ createdAt: now,
488
+ status: 'completed',
489
+ },
490
+ { ...agentMessage(), id: 'a2' },
491
+ ],
492
+ },
493
+ });
494
+
495
+ expect(agentRowBorders(container)).toEqual([true, false]);
496
+ });
497
+
498
+ it('hides the separator on the only assistant message', () => {
499
+ const { container } = render(CkConversation, {
500
+ props: { messages: [{ ...agentMessage(), id: 'solo' }] },
501
+ });
502
+
503
+ expect(agentRowBorders(container)).toEqual([false]);
504
+ });
505
+ });
506
+
432
507
  describe('empty conversation', () => {
433
508
  it('renders nothing when messages array is empty', () => {
434
509
  const { container } = render(CkConversation, { props: { messages: [] } });
@@ -2,6 +2,7 @@
2
2
  import { useChatKitLabels } from '../../../handlers/useChatKitLabels';
3
3
  import type { BaseMessage, MessageTextContent } from '../../../types';
4
4
  import CkAgentMessage from '../../base/CkAgentMessage/CkAgentMessage.vue';
5
+ import CkAgentMessageHeader from '../../base/CkAgentMessageHeader/CkAgentMessageHeader.vue';
5
6
  import CkConversationLayout from '../../base/CkConversationLayout/CkConversationLayout.vue';
6
7
  import CkCopyMessageButton from '../../base/CkCopyMessageButton/CkCopyMessageButton.vue';
7
8
  import CkMarkdownRenderer from '../../base/CkMarkdownRenderer/CkMarkdownRenderer.vue';
@@ -34,8 +35,16 @@ const slots = defineSlots<{
34
35
  'agent-message-icon': (props: { message: BaseMessage }) => void;
35
36
  /** Replace the agent loading icon shown while a message is being generated */
36
37
  'agent-message-loading-icon': (props: { message: BaseMessage }) => void;
37
- /** Replace the agent message header */
38
+ /** Replace the entire agent message header (the whole `CkAgentMessageHeader`) */
38
39
  'agent-message-header': (props: { message: BaseMessage }) => void;
40
+ /** Replace the agent message header text (rendered inside the header row) */
41
+ 'agent-message-header-content': (props: { message: BaseMessage }) => void;
42
+ /**
43
+ * Provide an activity control for the agent message header (rendered on the
44
+ * right of the header, typically a `CkHeaderActivityButton`). When provided,
45
+ * the header gains vertical padding and a bottom border.
46
+ */
47
+ 'agent-message-header-activity': (props: { message: BaseMessage }) => void;
39
48
  /**
40
49
  * Replace the agent message body. Receives the full `BaseMessage` so the
41
50
  * consumer can inspect every content part (text, tool calls, reasoning)
@@ -64,6 +73,18 @@ const hasIcon = computed(
64
73
  const isLoading = (message: BaseMessage) =>
65
74
  message.status === 'started' || message.status === 'sending';
66
75
 
76
+ /**
77
+ * Id of the last assistant message. Its bottom separator is hidden so the
78
+ * border divides user/assistant pairs without a trailing line after the final
79
+ * pair.
80
+ */
81
+ const lastAgentMessageId = computed(() => {
82
+ for (let i = props.messages.length - 1; i >= 0; i--) {
83
+ if (props.messages[i].role !== 'user') return props.messages[i].id;
84
+ }
85
+ return undefined;
86
+ });
87
+
67
88
  /** Concatenated text from every text content part in a message. */
68
89
  function getMessageText(message: BaseMessage): string {
69
90
  return message.content
@@ -89,42 +110,64 @@ function getMessageText(message: BaseMessage): string {
89
110
 
90
111
  <template #agent-message="{ message }">
91
112
  <slot name="agent-message" :message="message">
92
- <CkAgentMessage>
93
- <template v-if="hasIcon" #icon>
94
- <template v-if="isLoading(message)">
95
- <slot name="agent-message-loading-icon" :message="message">
96
- <UiAvatar v-if="agentLoadingImage" class="size-6">
97
- <UiAvatarImage
98
- :src="agentLoadingImage"
99
- :alt="labels.agentLoadingAvatarAlt"
100
- />
101
- </UiAvatar>
102
- <UiSpinner
103
- v-else
104
- class="size-6"
105
- :aria-label="labels.agentLoadingAvatarAlt"
106
- />
107
- </slot>
108
- </template>
113
+ <CkAgentMessage :bottom-separator="message.id !== lastAgentMessageId">
114
+ <template
115
+ v-if="
116
+ hasIcon ||
117
+ $slots['agent-message-header'] ||
118
+ $slots['agent-message-header-content'] ||
119
+ $slots['agent-message-header-activity']
120
+ "
121
+ #header
122
+ >
123
+ <slot name="agent-message-header" :message="message">
124
+ <CkAgentMessageHeader>
125
+ <template v-if="hasIcon" #icon>
126
+ <template v-if="isLoading(message)">
127
+ <slot name="agent-message-loading-icon" :message="message">
128
+ <UiAvatar v-if="agentLoadingImage" class="size-8">
129
+ <UiAvatarImage
130
+ :src="agentLoadingImage"
131
+ :alt="labels.agentLoadingAvatarAlt"
132
+ />
133
+ </UiAvatar>
134
+ <UiSpinner
135
+ v-else
136
+ class="size-8"
137
+ :aria-label="labels.agentLoadingAvatarAlt"
138
+ />
139
+ </slot>
140
+ </template>
109
141
 
110
- <template v-else>
111
- <slot name="agent-message-icon" :message="message">
112
- <UiAvatar class="size-6">
113
- <UiAvatarImage
114
- v-if="agentAvatarImage"
115
- :src="agentAvatarImage"
116
- :alt="labels.agentAvatarAlt"
117
- />
118
- <UiAvatarFallback v-if="agentAvatarFallback">{{
119
- agentAvatarFallback
120
- }}</UiAvatarFallback>
121
- </UiAvatar>
122
- </slot>
123
- </template>
124
- </template>
142
+ <template v-else>
143
+ <slot name="agent-message-icon" :message="message">
144
+ <UiAvatar class="size-8">
145
+ <UiAvatarImage
146
+ v-if="agentAvatarImage"
147
+ :src="agentAvatarImage"
148
+ :alt="labels.agentAvatarAlt"
149
+ />
150
+ <UiAvatarFallback v-if="agentAvatarFallback">{{
151
+ agentAvatarFallback
152
+ }}</UiAvatarFallback>
153
+ </UiAvatar>
154
+ </slot>
155
+ </template>
156
+ </template>
157
+
158
+ <slot name="agent-message-header-content" :message="message" />
125
159
 
126
- <template v-if="$slots['agent-message-header']" #header>
127
- <slot name="agent-message-header" :message="message" />
160
+ <template
161
+ v-if="$slots['agent-message-header-activity']"
162
+ #activity
163
+ >
164
+ <slot
165
+ name="agent-message-header-activity"
166
+ :message="message"
167
+ />
168
+ </template>
169
+ </CkAgentMessageHeader>
170
+ </slot>
128
171
  </template>
129
172
 
130
173
  <slot name="agent-message-content" :message="message">
@@ -10,7 +10,7 @@ import type { CkMarkdownElementClasses } from '../../base/CkMarkdownRenderer';
10
10
  * @category Messages
11
11
  * @useCases complete chat conversation ui, streaming chat with markdown, assistant chat view, llm conversation component, drop-in chat surface
12
12
  * @keywords conversation, chat, messages, assistant, agent, user, markdown, streaming, avatar
13
- * @related CkConversationLayout, CkAgentMessage, CkUserMessage, CkMarkdownRenderer, CkCopyMessageButton, CkInput
13
+ * @related CkConversationLayout, CkAgentMessage, CkUserMessage, CkMarkdownRenderer, CkCopyMessageButton, CkPromptInput
14
14
  */
15
15
  export interface CkConversationProps {
16
16
  /** List of messages to display, in chronological order. */
@@ -2,4 +2,3 @@ export { CkConversation } from './CkConversation';
2
2
  export type { CkConversationProps } from './CkConversation';
3
3
  export { CkHistory } from './CkHistory';
4
4
  export type { CkHistoryEmits, CkHistoryProps } from './CkHistory';
5
- export { CkInput } from './CkInput';
@@ -1,6 +1,8 @@
1
1
  // Base (presentational) components
2
2
  export {
3
3
  CkAgentMessage,
4
+ CkAgentMessageHeader,
5
+ CkHeaderActivityButton,
4
6
  CkCitation,
5
7
  CkCitationCard,
6
8
  CkConversationLayout,
@@ -25,6 +27,9 @@ export {
25
27
  CkUserMessage,
26
28
  } from './base';
27
29
  export type {
30
+ CkAgentMessageHeaderProps,
31
+ CkHeaderActivityButtonProps,
32
+ CkHeaderActivityButtonEmits,
28
33
  CkCitationProps,
29
34
  CkCitationEmits,
30
35
  CkCitationSource,
@@ -59,7 +64,7 @@ export type {
59
64
  } from './base';
60
65
 
61
66
  // Composed (pattern) components
62
- export { CkConversation, CkHistory, CkInput } from './composed';
67
+ export { CkConversation, CkHistory } from './composed';
63
68
  export type {
64
69
  CkConversationProps,
65
70
  CkHistoryEmits,
package/src/index.ts CHANGED
@@ -63,6 +63,8 @@ export type {
63
63
  // Components
64
64
  export {
65
65
  CkAgentMessage,
66
+ CkAgentMessageHeader,
67
+ CkHeaderActivityButton,
66
68
  CkCitation,
67
69
  CkCitationCard,
68
70
  CkConversation,
@@ -75,7 +77,6 @@ export {
75
77
  CkHistory,
76
78
  CkHistoryItem,
77
79
  CkHistoryWrapper,
78
- CkInput,
79
80
  CkMarkdownRenderer,
80
81
  CkMessageButton,
81
82
  CkModelSelector,
@@ -90,6 +91,9 @@ export {
90
91
  CkUserMessage,
91
92
  } from './components';
92
93
  export type {
94
+ CkAgentMessageHeaderProps,
95
+ CkHeaderActivityButtonProps,
96
+ CkHeaderActivityButtonEmits,
93
97
  CkCitationProps,
94
98
  CkCitationEmits,
95
99
  CkCitationSource,
@@ -1,43 +0,0 @@
1
- import CkInput from './CkInput.vue';
2
- import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
- declare const meta: Meta<typeof CkInput>;
4
- export default meta;
5
- type Story = StoryObj<typeof meta>;
6
- /**
7
- * Default chat input with disclaimer and the built-in send button.
8
- * Use as the primary way to let users submit a prompt to the assistant.
9
- */
10
- export declare const Default: Story;
11
- /**
12
- * Adds a mode toggle (Express/Smart) to the left side of the toolbar via
13
- * `#toolbar-left`. Use when the user needs to switch between assistant
14
- * presets or capabilities before submitting.
15
- */
16
- export declare const WithModeToggle: Story;
17
- /**
18
- * End-to-end example: mode toggle on the left, attachment button + custom
19
- * send button on the right, and a disclaimer underneath. Use as a reference
20
- * for a production-ready chat compose UI.
21
- */
22
- export declare const FullExample: Story;
23
- /**
24
- * Disabled state — the textarea is read-only and the send button is inactive.
25
- * Use while waiting for an upstream action to resolve.
26
- */
27
- export declare const Disabled: Story;
28
- /**
29
- * Loading state — shown while the assistant is generating a reply. The
30
- * send button displays a spinner and the input is blocked.
31
- */
32
- export declare const Loading: Story;
33
- /**
34
- * Demonstrates the textarea's auto-grow behavior with multi-paragraph content.
35
- * Use when validating how the input handles long prompts.
36
- */
37
- export declare const WithLongText: Story;
38
- /**
39
- * Narrow viewport preview (≈320px). Use to verify the input stays usable on
40
- * small screens.
41
- */
42
- export declare const MobileWidth: Story;
43
- //# sourceMappingURL=CkInput.stories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"CkInput.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkInput/CkInput.stories.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,eAAe,CAAC;AACpC,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAM3D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,OAAO,CAqC9B,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAqGnC;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,KAerB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,KAgD5B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,KA2FzB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,QAAQ,EAAE,KAiBtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,KA2BrB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,KAuB1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,KAsBzB,CAAC"}