@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.
- 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
- package/dist/chat-kit.css +28 -6
- package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +14 -13
- package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
- package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts +8 -7
- package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts.map +1 -1
- package/dist/components/base/CkAgentMessage/types.d.ts +16 -5
- package/dist/components/base/CkAgentMessage/types.d.ts.map +1 -1
- package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.d.ts +16 -0
- package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.d.ts.map +1 -0
- package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue.d.ts +21 -0
- package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue.d.ts.map +1 -0
- package/dist/components/base/CkAgentMessageHeader/index.d.ts +3 -0
- package/dist/components/base/CkAgentMessageHeader/index.d.ts.map +1 -0
- package/dist/components/base/CkAgentMessageHeader/types.d.ts +22 -0
- package/dist/components/base/CkAgentMessageHeader/types.d.ts.map +1 -0
- package/dist/components/base/CkCitation/CkCitation.stories.d.ts +6 -0
- package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -1
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
- package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +5 -4
- package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -1
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -1
- package/dist/components/base/CkFileItemsList/CkFileItemsList.stories.d.ts +15 -4
- package/dist/components/base/CkFileItemsList/CkFileItemsList.stories.d.ts.map +1 -1
- package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +2 -1
- package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts.map +1 -1
- package/dist/components/base/CkFileItemsList/types.d.ts +17 -4
- package/dist/components/base/CkFileItemsList/types.d.ts.map +1 -1
- package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.d.ts +19 -0
- package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.d.ts.map +1 -0
- package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue.d.ts +22 -0
- package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkHeaderActivityButton/index.d.ts +3 -0
- package/dist/components/base/CkHeaderActivityButton/index.d.ts.map +1 -0
- package/dist/components/base/CkHeaderActivityButton/types.d.ts +36 -0
- package/dist/components/base/CkHeaderActivityButton/types.d.ts.map +1 -0
- package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts +6 -6
- package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptInput/types.d.ts +4 -4
- package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptInputDisclaimer/types.d.ts +1 -1
- package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
- package/dist/components/base/CkPromptSendButton/types.d.ts +1 -1
- package/dist/components/base/CkPromptStopButton/types.d.ts +1 -1
- package/dist/components/base/index.d.ts +4 -0
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +13 -1
- package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/types.d.ts +1 -1
- package/dist/components/composed/index.d.ts +0 -1
- package/dist/components/composed/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +26 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -26
- package/package.json +1 -1
- package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +12 -17
- package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +124 -113
- package/src/components/base/CkAgentMessage/CkAgentMessage.vue +18 -16
- package/src/components/base/CkAgentMessage/types.ts +16 -5
- package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.spec.ts +64 -0
- package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.ts +90 -0
- package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue +47 -0
- package/src/components/base/CkAgentMessageHeader/index.ts +2 -0
- package/src/components/base/CkAgentMessageHeader/types.ts +21 -0
- package/src/components/base/CkCitation/CkCitation.stories.ts +12 -0
- package/src/components/base/CkCitation/CkCitation.vue +34 -9
- package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +5 -4
- package/src/components/base/CkCitationCard/CkCitationCard.vue +2 -2
- package/src/components/base/CkConversationLayout/CkConversationLayout.vue +1 -1
- package/src/components/base/CkFileItemsList/CkFileItemsList.spec.ts +88 -13
- package/src/components/base/CkFileItemsList/CkFileItemsList.stories.ts +61 -13
- package/src/components/base/CkFileItemsList/CkFileItemsList.vue +153 -110
- package/src/components/base/CkFileItemsList/types.ts +17 -4
- package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.spec.ts +50 -0
- package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.ts +102 -0
- package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue +39 -0
- package/src/components/base/CkHeaderActivityButton/index.ts +2 -0
- package/src/components/base/CkHeaderActivityButton/types.ts +36 -0
- package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +88 -0
- package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +6 -6
- package/src/components/base/CkPromptInput/CkPromptInput.vue +63 -12
- package/src/components/base/CkPromptInput/types.ts +4 -4
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +1 -1
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +1 -1
- package/src/components/base/CkPromptInputDisclaimer/types.ts +1 -1
- package/src/components/base/CkPromptSendButton/types.ts +1 -1
- package/src/components/base/CkPromptStopButton/types.ts +1 -1
- package/src/components/base/index.ts +7 -0
- package/src/components/composed/CkConversation/CkConversation.spec.ts +81 -6
- package/src/components/composed/CkConversation/CkConversation.vue +78 -35
- package/src/components/composed/CkConversation/types.ts +1 -1
- package/src/components/composed/index.ts +0 -1
- package/src/components/index.ts +6 -1
- package/src/index.ts +5 -1
- package/dist/components/composed/CkInput/CkInput.stories.d.ts +0 -43
- package/dist/components/composed/CkInput/CkInput.stories.d.ts.map +0 -1
- package/dist/components/composed/CkInput/CkInput.vue.d.ts +0 -50
- package/dist/components/composed/CkInput/CkInput.vue.d.ts.map +0 -1
- package/dist/components/composed/CkInput/index.d.ts +0 -3
- package/dist/components/composed/CkInput/index.d.ts.map +0 -1
- package/dist/components/composed/CkInput/types.d.ts +0 -55
- package/dist/components/composed/CkInput/types.d.ts.map +0 -1
- package/src/components/composed/CkInput/CkInput.spec.ts +0 -191
- package/src/components/composed/CkInput/CkInput.stories.ts +0 -434
- package/src/components/composed/CkInput/CkInput.vue +0 -106
- package/src/components/composed/CkInput/index.ts +0 -2
- package/src/components/composed/CkInput/types.ts +0 -55
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
UiProgress,
|
|
9
9
|
type UiFileItemSize,
|
|
10
10
|
} from '@aleph-alpha/ui-library';
|
|
11
|
-
import { computed, ref } from 'vue';
|
|
11
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
12
12
|
import CkFileItemRow from './CkFileItemRow.vue';
|
|
13
13
|
import type {
|
|
14
14
|
CkFileEntry,
|
|
@@ -21,7 +21,8 @@ defineOptions({
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
const props = withDefaults(defineProps<CkFileItemsListProps>(), {
|
|
24
|
-
|
|
24
|
+
minItemWidth: 200,
|
|
25
|
+
maxItemWidth: 256,
|
|
25
26
|
size: 'small',
|
|
26
27
|
isRemovable: false,
|
|
27
28
|
removeLabel: 'Remove file',
|
|
@@ -48,7 +49,42 @@ defineSlots<{
|
|
|
48
49
|
}) => unknown;
|
|
49
50
|
}>();
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
// Gap between inline items (`gap-2` => 0.5rem => 8px).
|
|
53
|
+
const ITEM_GAP = 8;
|
|
54
|
+
|
|
55
|
+
const rootRef = ref<HTMLElement | null>(null);
|
|
56
|
+
const containerWidth = ref(0);
|
|
57
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
58
|
+
|
|
59
|
+
onMounted(() => {
|
|
60
|
+
const el = rootRef.value;
|
|
61
|
+
if (!el) return;
|
|
62
|
+
containerWidth.value = el.clientWidth;
|
|
63
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
64
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
containerWidth.value = entry.contentRect.width;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
resizeObserver.observe(el);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
onBeforeUnmount(() => resizeObserver?.disconnect());
|
|
74
|
+
|
|
75
|
+
// Number of items that fit at the current width while each stays at least
|
|
76
|
+
// `minItemWidth` wide. There is no upper cap — as many items as fit are kept
|
|
77
|
+
// inline. Falls back to showing every file before the first measurement (SSR /
|
|
78
|
+
// initial render / environments without ResizeObserver) so the list never
|
|
79
|
+
// flashes collapsed; the observer corrects it on mount.
|
|
80
|
+
const inlineCapacity = computed(() => {
|
|
81
|
+
const width = containerWidth.value;
|
|
82
|
+
if (!width) return props.files.length;
|
|
83
|
+
const fit = Math.floor((width + ITEM_GAP) / (props.minItemWidth + ITEM_GAP));
|
|
84
|
+
return Math.max(1, fit);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const isCollapsed = computed(() => props.files.length > inlineCapacity.value);
|
|
52
88
|
const isOpen = ref(false);
|
|
53
89
|
|
|
54
90
|
function isRowDisabled(file: CkFileEntry): boolean {
|
|
@@ -120,123 +156,130 @@ function handleRowClick(file: CkFileEntry, event: MouseEvent) {
|
|
|
120
156
|
</script>
|
|
121
157
|
|
|
122
158
|
<template>
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<li
|
|
130
|
-
v-for="file in files"
|
|
131
|
-
:key="file.id"
|
|
132
|
-
class="min-w-0 max-w-64 flex-1 basis-0"
|
|
133
|
-
@click="handleRowClick(file, $event)"
|
|
159
|
+
<div ref="rootRef" class="w-full min-w-0">
|
|
160
|
+
<ul
|
|
161
|
+
v-if="!isCollapsed"
|
|
162
|
+
data-slot="file-items-list"
|
|
163
|
+
class="m-0 flex w-full min-w-0 list-none flex-wrap items-center gap-2 p-0"
|
|
164
|
+
:aria-label="listLabel"
|
|
134
165
|
>
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
:
|
|
138
|
-
|
|
139
|
-
:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<slot name="item" v-bind="bindings" />
|
|
145
|
-
</template>
|
|
146
|
-
</CkFileItemRow>
|
|
147
|
-
</li>
|
|
148
|
-
</ul>
|
|
149
|
-
|
|
150
|
-
<UiPopover v-else v-model:open="isOpen">
|
|
151
|
-
<UiPopoverTrigger as-child>
|
|
152
|
-
<button
|
|
153
|
-
type="button"
|
|
154
|
-
data-slot="file-items-list-trigger"
|
|
155
|
-
:disabled="disabled"
|
|
156
|
-
class="relative flex w-full max-w-64 min-w-0 items-center gap-3 rounded-xl bg-background-surface-muted-subtle text-left transition-colors hover:bg-background-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-background-surface-muted-subtle"
|
|
157
|
-
:class="size === 'small' ? 'p-2' : 'p-3'"
|
|
158
|
-
:aria-label="resolvedTriggerAriaLabel"
|
|
166
|
+
<li
|
|
167
|
+
v-for="file in files"
|
|
168
|
+
:key="file.id"
|
|
169
|
+
class="flex-1 basis-0"
|
|
170
|
+
:style="{
|
|
171
|
+
minWidth: `${minItemWidth}px`,
|
|
172
|
+
maxWidth: `${maxItemWidth}px`,
|
|
173
|
+
}"
|
|
174
|
+
@click="handleRowClick(file, $event)"
|
|
159
175
|
>
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
v-if="!showStatus"
|
|
168
|
-
class="min-w-0 flex-1 truncate text-content-on-surface-primary"
|
|
169
|
-
:class="size === 'small' ? 'label-12-medium' : 'body-14-medium'"
|
|
176
|
+
<CkFileItemRow
|
|
177
|
+
:file="file"
|
|
178
|
+
:size="size"
|
|
179
|
+
:is-removable="isRemovable"
|
|
180
|
+
:remove-label="removeLabel"
|
|
181
|
+
:disabled="isRowDisabled(file)"
|
|
182
|
+
@remove="handleRemove(file)"
|
|
170
183
|
>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
<template #default="bindings">
|
|
185
|
+
<slot name="item" v-bind="bindings" />
|
|
186
|
+
</template>
|
|
187
|
+
</CkFileItemRow>
|
|
188
|
+
</li>
|
|
189
|
+
</ul>
|
|
190
|
+
|
|
191
|
+
<UiPopover v-else v-model:open="isOpen">
|
|
192
|
+
<UiPopoverTrigger as-child>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
data-slot="file-items-list-trigger"
|
|
196
|
+
:disabled="disabled"
|
|
197
|
+
class="relative flex w-full max-w-64 min-w-0 items-center gap-3 rounded-xl bg-background-surface-muted-subtle text-left transition-colors hover:bg-background-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-background-surface-muted-subtle"
|
|
198
|
+
:class="size === 'small' ? 'p-2' : 'p-3'"
|
|
199
|
+
:aria-label="resolvedTriggerAriaLabel"
|
|
200
|
+
>
|
|
201
|
+
<UiFileType
|
|
202
|
+
v-if="showStatus && hasFailures && !uploadInProgress"
|
|
203
|
+
:error="true"
|
|
204
|
+
/>
|
|
205
|
+
<UiFileType v-else icon="paperclip" tone="default" />
|
|
206
|
+
|
|
174
207
|
<span
|
|
175
|
-
|
|
208
|
+
v-if="!showStatus"
|
|
209
|
+
class="min-w-0 flex-1 truncate text-content-on-surface-primary"
|
|
176
210
|
:class="size === 'small' ? 'label-12-medium' : 'body-14-medium'"
|
|
177
211
|
>
|
|
178
212
|
{{ resolvedTriggerLabel }}
|
|
179
213
|
</span>
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
214
|
+
<div v-else class="flex min-w-0 flex-1 flex-col items-start gap-0.5">
|
|
215
|
+
<span
|
|
216
|
+
class="w-full truncate text-content-on-surface-primary"
|
|
217
|
+
:class="size === 'small' ? 'label-12-medium' : 'body-14-medium'"
|
|
218
|
+
>
|
|
219
|
+
{{ resolvedTriggerLabel }}
|
|
220
|
+
</span>
|
|
221
|
+
<UiProgress
|
|
222
|
+
v-if="uploadInProgress"
|
|
223
|
+
:model-value="aggregateProgress"
|
|
224
|
+
class="h-1 w-full"
|
|
225
|
+
:aria-label="`${resolvedTriggerLabel} upload progress`"
|
|
226
|
+
/>
|
|
227
|
+
<span
|
|
228
|
+
v-else-if="hasFailures"
|
|
229
|
+
class="label-12-medium w-full truncate text-content-on-status-error-inverse"
|
|
230
|
+
>
|
|
231
|
+
{{ failedStatusLabel }}
|
|
232
|
+
</span>
|
|
233
|
+
<span
|
|
234
|
+
v-else-if="triggerLabel"
|
|
235
|
+
class="label-12-medium w-full truncate text-content-on-surface-muted"
|
|
236
|
+
>
|
|
237
|
+
{{ countStatusLabel }}
|
|
238
|
+
</span>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<UiIcon
|
|
242
|
+
name="chevron-down"
|
|
243
|
+
:size="16"
|
|
244
|
+
aria-hidden="true"
|
|
245
|
+
class="shrink-0 text-content-on-surface-muted transition-transform"
|
|
246
|
+
:class="isOpen && 'rotate-180'"
|
|
185
247
|
/>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
v-else-if="triggerLabel"
|
|
194
|
-
class="label-12-medium w-full truncate text-content-on-surface-muted"
|
|
195
|
-
>
|
|
196
|
-
{{ countStatusLabel }}
|
|
197
|
-
</span>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<UiIcon
|
|
201
|
-
name="chevron-down"
|
|
202
|
-
:size="16"
|
|
203
|
-
aria-hidden="true"
|
|
204
|
-
class="shrink-0 text-content-on-surface-muted transition-transform"
|
|
205
|
-
:class="isOpen && 'rotate-180'"
|
|
206
|
-
/>
|
|
207
|
-
</button>
|
|
208
|
-
</UiPopoverTrigger>
|
|
209
|
-
|
|
210
|
-
<UiPopoverContent
|
|
211
|
-
align="start"
|
|
212
|
-
:side-offset="4"
|
|
213
|
-
:class="['w-64 p-2', popoverClass]"
|
|
214
|
-
>
|
|
215
|
-
<ul
|
|
216
|
-
data-slot="file-items-list-collapsed"
|
|
217
|
-
class="m-0 flex list-none flex-col gap-2 p-0"
|
|
218
|
-
:aria-label="listLabel"
|
|
248
|
+
</button>
|
|
249
|
+
</UiPopoverTrigger>
|
|
250
|
+
|
|
251
|
+
<UiPopoverContent
|
|
252
|
+
align="start"
|
|
253
|
+
:side-offset="4"
|
|
254
|
+
:class="['w-64 p-2', popoverClass]"
|
|
219
255
|
>
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
@click="handleRowClick(file, $event)"
|
|
256
|
+
<ul
|
|
257
|
+
data-slot="file-items-list-collapsed"
|
|
258
|
+
class="m-0 flex list-none flex-col gap-2 p-0"
|
|
259
|
+
:aria-label="listLabel"
|
|
225
260
|
>
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
:
|
|
229
|
-
|
|
230
|
-
:
|
|
231
|
-
|
|
232
|
-
@remove="handleRemove(file)"
|
|
261
|
+
<li
|
|
262
|
+
v-for="file in files"
|
|
263
|
+
:key="file.id"
|
|
264
|
+
class="min-w-0"
|
|
265
|
+
:style="{ maxWidth: `${maxItemWidth}px` }"
|
|
266
|
+
@click="handleRowClick(file, $event)"
|
|
233
267
|
>
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
268
|
+
<CkFileItemRow
|
|
269
|
+
:file="file"
|
|
270
|
+
:size="size"
|
|
271
|
+
:is-removable="isRemovable"
|
|
272
|
+
:remove-label="removeLabel"
|
|
273
|
+
:disabled="isRowDisabled(file)"
|
|
274
|
+
@remove="handleRemove(file)"
|
|
275
|
+
>
|
|
276
|
+
<template #default="bindings">
|
|
277
|
+
<slot name="item" v-bind="bindings" />
|
|
278
|
+
</template>
|
|
279
|
+
</CkFileItemRow>
|
|
280
|
+
</li>
|
|
281
|
+
</ul>
|
|
282
|
+
</UiPopoverContent>
|
|
283
|
+
</UiPopover>
|
|
284
|
+
</div>
|
|
242
285
|
</template>
|
|
@@ -25,8 +25,9 @@ export interface CkFileEntry {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Horizontal list of file attachments
|
|
29
|
-
*
|
|
28
|
+
* Horizontal list of file attachments. Renders as many items inline as fit the
|
|
29
|
+
* available width — each kept between `minItemWidth` and `maxItemWidth` wide —
|
|
30
|
+
* and collapses into a popover once they no longer fit.
|
|
30
31
|
*
|
|
31
32
|
* @category Form Inputs
|
|
32
33
|
* @useCases user message attachments, prompt input previews, chat file row
|
|
@@ -36,8 +37,20 @@ export interface CkFileEntry {
|
|
|
36
37
|
export interface CkFileItemsListProps {
|
|
37
38
|
/** Files to render. Order is preserved. */
|
|
38
39
|
files: CkFileEntry[];
|
|
39
|
-
/**
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Minimum width (px) an inline item may occupy. As the wrapper narrows, fewer
|
|
42
|
+
* items stay inline (each kept at least this wide) until the list collapses
|
|
43
|
+
* into the popover. Lower values keep more items inline on narrow widths.
|
|
44
|
+
* @default 200
|
|
45
|
+
*/
|
|
46
|
+
minItemWidth?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Maximum width (px) an inline item may grow to. Items never stretch beyond
|
|
49
|
+
* this width; any extra horizontal space is left unused. There is no cap on
|
|
50
|
+
* the number of inline items as long as they fit the wrapper.
|
|
51
|
+
* @default 256
|
|
52
|
+
*/
|
|
53
|
+
maxItemWidth?: number;
|
|
41
54
|
/** @default 'small' */
|
|
42
55
|
size?: UiFileItemSize;
|
|
43
56
|
/** Show a remove control on every row. @default false */
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import CkHeaderActivityButton from './CkHeaderActivityButton.vue';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen } from '@testing-library/vue';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('CkHeaderActivityButton', () => {
|
|
7
|
+
it('renders the slot content as the label', () => {
|
|
8
|
+
render(CkHeaderActivityButton, {
|
|
9
|
+
slots: { default: '1 tool, 42 sources' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(
|
|
13
|
+
screen.getByRole('button', { name: /1 tool, 42 sources/ }),
|
|
14
|
+
).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('uses the explicit ariaLabel when provided', () => {
|
|
18
|
+
render(CkHeaderActivityButton, {
|
|
19
|
+
props: { ariaLabel: 'Show activity' },
|
|
20
|
+
slots: { default: '1 tool' },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(
|
|
24
|
+
screen.getByRole('button', { name: 'Show activity' }),
|
|
25
|
+
).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('emits click when pressed', async () => {
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
const { emitted } = render(CkHeaderActivityButton, {
|
|
31
|
+
slots: { default: 'Activity' },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await user.click(screen.getByRole('button'));
|
|
35
|
+
|
|
36
|
+
expect(emitted('click')).toHaveLength(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not emit click when disabled', async () => {
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
const { emitted } = render(CkHeaderActivityButton, {
|
|
42
|
+
props: { disabled: true },
|
|
43
|
+
slots: { default: 'Activity' },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await user.click(screen.getByRole('button'));
|
|
47
|
+
|
|
48
|
+
expect(emitted('click')).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import CkHeaderActivityButton from './CkHeaderActivityButton.vue';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof CkHeaderActivityButton> = {
|
|
5
|
+
title: 'Base/CkHeaderActivityButton',
|
|
6
|
+
component: CkHeaderActivityButton,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
disabled: {
|
|
10
|
+
control: 'boolean',
|
|
11
|
+
description: 'Disables the button and prevents the `click` event.',
|
|
12
|
+
},
|
|
13
|
+
ariaLabel: {
|
|
14
|
+
control: 'text',
|
|
15
|
+
description:
|
|
16
|
+
'Accessible label. Falls back to the label text when omitted.',
|
|
17
|
+
},
|
|
18
|
+
icon: {
|
|
19
|
+
control: 'text',
|
|
20
|
+
description:
|
|
21
|
+
"Trailing Lucide icon name. Set to '' to hide. Defaults to 'arrow-right'.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
decorators: [
|
|
25
|
+
() => ({
|
|
26
|
+
template: '<div style="padding: 24px;"><story /></div>',
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
type Story = StoryObj<typeof meta>;
|
|
33
|
+
|
|
34
|
+
const defaultTemplateSource = `<script setup lang="ts">
|
|
35
|
+
import { CkHeaderActivityButton } from '@aleph-alpha/chat-kit';
|
|
36
|
+
|
|
37
|
+
function openActivity() {
|
|
38
|
+
// open the activity / chain-of-thought panel
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<CkHeaderActivityButton @click="openActivity">
|
|
44
|
+
1 tool, 42 sources
|
|
45
|
+
</CkHeaderActivityButton>
|
|
46
|
+
</template>`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Default activity button: a short label followed by the trailing arrow icon.
|
|
50
|
+
* It emits a generic `click` and performs no action on its own.
|
|
51
|
+
*/
|
|
52
|
+
export const Default: Story = {
|
|
53
|
+
render: (args) => ({
|
|
54
|
+
components: { CkHeaderActivityButton },
|
|
55
|
+
setup() {
|
|
56
|
+
return { args };
|
|
57
|
+
},
|
|
58
|
+
template: `
|
|
59
|
+
<CkHeaderActivityButton v-bind="args">
|
|
60
|
+
1 tool, 42 sources
|
|
61
|
+
</CkHeaderActivityButton>
|
|
62
|
+
`,
|
|
63
|
+
}),
|
|
64
|
+
args: { disabled: false, icon: 'arrow-right' },
|
|
65
|
+
parameters: { docs: { source: { code: defaultTemplateSource } } },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Without a trailing icon — pass an empty `icon` to render the label only.
|
|
70
|
+
*/
|
|
71
|
+
export const WithoutIcon: Story = {
|
|
72
|
+
render: (args) => ({
|
|
73
|
+
components: { CkHeaderActivityButton },
|
|
74
|
+
setup() {
|
|
75
|
+
return { args };
|
|
76
|
+
},
|
|
77
|
+
template: `
|
|
78
|
+
<CkHeaderActivityButton v-bind="args">
|
|
79
|
+
3 sources
|
|
80
|
+
</CkHeaderActivityButton>
|
|
81
|
+
`,
|
|
82
|
+
}),
|
|
83
|
+
args: { icon: '' },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Disabled state: the button is non-interactive and does not emit `click`.
|
|
88
|
+
*/
|
|
89
|
+
export const Disabled: Story = {
|
|
90
|
+
render: (args) => ({
|
|
91
|
+
components: { CkHeaderActivityButton },
|
|
92
|
+
setup() {
|
|
93
|
+
return { args };
|
|
94
|
+
},
|
|
95
|
+
template: `
|
|
96
|
+
<CkHeaderActivityButton v-bind="args">
|
|
97
|
+
1 tool, 42 sources
|
|
98
|
+
</CkHeaderActivityButton>
|
|
99
|
+
`,
|
|
100
|
+
}),
|
|
101
|
+
args: { disabled: true },
|
|
102
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CkHeaderActivityButtonProps } from './types';
|
|
3
|
+
import { UiButton, UiIcon } from '@aleph-alpha/ui-library';
|
|
4
|
+
|
|
5
|
+
defineOptions({
|
|
6
|
+
name: 'CkHeaderActivityButton',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<CkHeaderActivityButtonProps>(), {
|
|
10
|
+
disabled: false,
|
|
11
|
+
icon: 'arrow-right',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
click: [];
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
function handleClick() {
|
|
19
|
+
if (props.disabled) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
emit('click');
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<UiButton
|
|
29
|
+
v-bind="{ ariaLabel }"
|
|
30
|
+
:disabled="disabled"
|
|
31
|
+
variant="ghost"
|
|
32
|
+
size="sm"
|
|
33
|
+
class="label-12-medium text-content-on-surface-muted gap-1 rounded-[3px] px-2 py-1"
|
|
34
|
+
@click="handleClick"
|
|
35
|
+
>
|
|
36
|
+
<slot />
|
|
37
|
+
<UiIcon v-if="icon" :name="icon" :size="12" aria-hidden="true" />
|
|
38
|
+
</UiButton>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact, ghost-styled button shown on the right of an agent message header
|
|
3
|
+
* (see `CkAgentMessageHeader`). Renders a short label followed by a trailing
|
|
4
|
+
* icon (an arrow by default) and emits a generic `click` event — it performs
|
|
5
|
+
* no action of its own, leaving the consumer to decide what happens (e.g. open
|
|
6
|
+
* an activity / chain-of-thought panel).
|
|
7
|
+
*
|
|
8
|
+
* @category Actions
|
|
9
|
+
* @useCases agent message header action, open activity panel, chain-of-thought summary toggle, tool/source indicator
|
|
10
|
+
* @keywords header, activity, action, button, ghost, arrow, agent, message
|
|
11
|
+
* @related CkAgentMessageHeader, CkAgentMessage, CkConversation, CkMessageButton
|
|
12
|
+
*/
|
|
13
|
+
export interface CkHeaderActivityButtonProps {
|
|
14
|
+
/**
|
|
15
|
+
* Whether the button is disabled and ignores clicks.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Accessible label for the button. Defaults to the rendered label text when
|
|
21
|
+
* omitted, but should be set when the slot content is not descriptive on its
|
|
22
|
+
* own.
|
|
23
|
+
*/
|
|
24
|
+
ariaLabel?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Trailing [Lucide](https://lucide.dev/icons) icon name shown after the
|
|
27
|
+
* label. Set to an empty string to hide the icon.
|
|
28
|
+
* @default 'arrow-right'
|
|
29
|
+
*/
|
|
30
|
+
icon?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CkHeaderActivityButtonEmits {
|
|
34
|
+
/** Emitted when the button is pressed and not disabled. */
|
|
35
|
+
(e: 'click'): void;
|
|
36
|
+
}
|
|
@@ -199,6 +199,94 @@ describe('CkPromptInput', () => {
|
|
|
199
199
|
});
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
// The focus highlight (border + ring) must appear for keyboard navigation
|
|
203
|
+
// only — not when the textarea is focused by a mouse click. The component
|
|
204
|
+
// distinguishes the two by remembering whether a pointerdown immediately
|
|
205
|
+
// preceded the focus event.
|
|
206
|
+
describe('keyboard vs mouse focus highlight', () => {
|
|
207
|
+
const HIGHLIGHT_CLASS = 'ring-2';
|
|
208
|
+
|
|
209
|
+
it('shows the highlight when focused via keyboard (no preceding pointer)', async () => {
|
|
210
|
+
const { container } = render(CkPromptInput);
|
|
211
|
+
|
|
212
|
+
await fireEvent.focus(screen.getByRole('textbox'));
|
|
213
|
+
|
|
214
|
+
const form = container.querySelector('form');
|
|
215
|
+
expect(form?.className).toContain(HIGHLIGHT_CLASS);
|
|
216
|
+
expect(form?.className).toContain('border-border-surface-focus-ring');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('does NOT show the highlight when focused via mouse click', async () => {
|
|
220
|
+
const { container } = render(CkPromptInput);
|
|
221
|
+
const textarea = screen.getByRole('textbox');
|
|
222
|
+
|
|
223
|
+
// A real mouse click fires pointerdown before focus.
|
|
224
|
+
await fireEvent.pointerDown(textarea);
|
|
225
|
+
await fireEvent.focus(textarea);
|
|
226
|
+
|
|
227
|
+
expect(container.querySelector('form')?.className).not.toContain(
|
|
228
|
+
HIGHLIGHT_CLASS,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('removes the highlight on blur', async () => {
|
|
233
|
+
const { container } = render(CkPromptInput);
|
|
234
|
+
const textarea = screen.getByRole('textbox');
|
|
235
|
+
|
|
236
|
+
await fireEvent.focus(textarea);
|
|
237
|
+
expect(container.querySelector('form')?.className).toContain(
|
|
238
|
+
HIGHLIGHT_CLASS,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
await fireEvent.blur(textarea);
|
|
242
|
+
expect(container.querySelector('form')?.className).not.toContain(
|
|
243
|
+
HIGHLIGHT_CLASS,
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('shows the highlight on a later keyboard focus after a prior mouse focus', async () => {
|
|
248
|
+
const { container } = render(CkPromptInput);
|
|
249
|
+
const textarea = screen.getByRole('textbox');
|
|
250
|
+
|
|
251
|
+
// First interaction: mouse click → no highlight.
|
|
252
|
+
await fireEvent.pointerDown(textarea);
|
|
253
|
+
await fireEvent.focus(textarea);
|
|
254
|
+
expect(container.querySelector('form')?.className).not.toContain(
|
|
255
|
+
HIGHLIGHT_CLASS,
|
|
256
|
+
);
|
|
257
|
+
await fireEvent.blur(textarea);
|
|
258
|
+
|
|
259
|
+
// Second interaction: tab in (focus with no preceding pointerdown). The
|
|
260
|
+
// pointer flag must have reset, so the highlight returns.
|
|
261
|
+
await fireEvent.focus(textarea);
|
|
262
|
+
expect(container.querySelector('form')?.className).toContain(
|
|
263
|
+
HIGHLIGHT_CLASS,
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('is not misclassified by a pointerdown inside an already-focused field', async () => {
|
|
268
|
+
const { container } = render(CkPromptInput);
|
|
269
|
+
const textarea = screen.getByRole('textbox');
|
|
270
|
+
|
|
271
|
+
// Keyboard focus → highlight on.
|
|
272
|
+
await fireEvent.focus(textarea);
|
|
273
|
+
expect(container.querySelector('form')?.className).toContain(
|
|
274
|
+
HIGHLIGHT_CLASS,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Click inside the already-focused field: pointerdown fires with no
|
|
278
|
+
// following focus, which would otherwise leave the pointer flag stale.
|
|
279
|
+
await fireEvent.pointerDown(textarea);
|
|
280
|
+
|
|
281
|
+
// Blur then keyboard-focus again — the stale flag must not suppress it.
|
|
282
|
+
await fireEvent.blur(textarea);
|
|
283
|
+
await fireEvent.focus(textarea);
|
|
284
|
+
expect(container.querySelector('form')?.className).toContain(
|
|
285
|
+
HIGHLIGHT_CLASS,
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
202
290
|
// The form switches between compact (single-row, rounded-3xl) and stacked
|
|
203
291
|
// (multi-row, rounded-2xl) layouts based on (a) the presence of the
|
|
204
292
|
// `toolbar-top` slot and (b) whether the textarea content exceeds one line.
|