@aleph-alpha/chat-kit 1.15.0 → 1.15.1
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-D8In_Dnl.js → CkInput.vue_vue_type_script_setup_true_lang-DedLzDoO.js} +134 -65
- package/dist/components/base/CkHistoryItem/CkHistoryItem.stories.d.ts.map +1 -1
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
- package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.d.ts.map +1 -1
- package/dist/components/composed/CkHistory/CkHistory.stories.d.ts +15 -0
- package/dist/components/composed/CkHistory/CkHistory.stories.d.ts.map +1 -1
- package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +6 -0
- package/dist/components/composed/CkHistory/CkHistory.vue.d.ts.map +1 -1
- package/dist/components/composed/CkHistory/types.d.ts +33 -0
- package/dist/components/composed/CkHistory/types.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useChatKitLabels.d.ts +5 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/helpers/formatRelativeTime.d.ts +10 -0
- package/dist/helpers/formatRelativeTime.d.ts.map +1 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/{useChatKitLabels-BxNkTzr2.js → useChatKitLabels-BC6ItJc6.js} +2 -1
- package/package.json +1 -1
- package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +0 -2
- package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +0 -6
- package/src/components/base/CkHistoryItem/CkHistoryItem.vue +2 -3
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +23 -23
- package/src/components/composed/CkHistory/CkHistory.spec.ts +107 -0
- package/src/components/composed/CkHistory/CkHistory.stories.ts +91 -3
- package/src/components/composed/CkHistory/CkHistory.vue +94 -44
- package/src/components/composed/CkHistory/types.ts +33 -0
- package/src/handlers/useChatKitLabels.spec.ts +1 -0
- package/src/handlers/useChatKitLabels.ts +6 -0
- package/src/helpers/formatRelativeTime.ts +49 -0
- package/src/helpers/index.ts +1 -0
- package/src/index.ts +3 -0
|
@@ -33,10 +33,14 @@ import { CkHistoryWrapper, CkHistoryItem } from '@aleph-alpha/chat-kit';
|
|
|
33
33
|
|
|
34
34
|
<template>
|
|
35
35
|
<CkHistoryWrapper>
|
|
36
|
-
<CkHistoryItem title="Project Proposal Overview" meta-start="Today"
|
|
37
|
-
<CkHistoryItem title="
|
|
38
|
-
<CkHistoryItem title="
|
|
39
|
-
<CkHistoryItem title="
|
|
36
|
+
<CkHistoryItem title="Project Proposal Overview" meta-start="Today" is-active />
|
|
37
|
+
<CkHistoryItem title="Customer onboarding flow" meta-start="Yesterday" />
|
|
38
|
+
<CkHistoryItem title="Refactor notes" meta-start="2 days ago" />
|
|
39
|
+
<CkHistoryItem title="Quarterly review prep" meta-start="3 days ago" />
|
|
40
|
+
<CkHistoryItem title="Sprint retro notes" meta-start="4 days ago" />
|
|
41
|
+
<CkHistoryItem title="Travel plans 2026" meta-start="1 week ago" />
|
|
42
|
+
<CkHistoryItem title="API design review" meta-start="2 weeks ago" />
|
|
43
|
+
<CkHistoryItem title="Bug triage session" meta-start="3 weeks ago" />
|
|
40
44
|
</CkHistoryWrapper>
|
|
41
45
|
</template>`;
|
|
42
46
|
|
|
@@ -48,7 +52,10 @@ import { CkHistoryWrapper, CkHistoryItem } from '@aleph-alpha/chat-kit';
|
|
|
48
52
|
<CkHistoryWrapper :with-gap="false">
|
|
49
53
|
<CkHistoryItem title="Thread A" meta-start="Today" />
|
|
50
54
|
<CkHistoryItem title="Thread B" meta-start="Yesterday" />
|
|
51
|
-
<CkHistoryItem title="Thread C" meta-start="
|
|
55
|
+
<CkHistoryItem title="Thread C" meta-start="2 days ago" />
|
|
56
|
+
<CkHistoryItem title="Thread D" meta-start="3 days ago" />
|
|
57
|
+
<CkHistoryItem title="Thread E" meta-start="4 days ago" />
|
|
58
|
+
<CkHistoryItem title="Thread F" meta-start="1 week ago" />
|
|
52
59
|
</CkHistoryWrapper>
|
|
53
60
|
</template>`;
|
|
54
61
|
|
|
@@ -65,24 +72,15 @@ export const Default: Story = {
|
|
|
65
72
|
{
|
|
66
73
|
title: 'Project Proposal Overview',
|
|
67
74
|
metaStart: 'Today',
|
|
68
|
-
metaEnd: 'Innsbruck',
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
title: 'Project Proposal Overview',
|
|
72
|
-
metaStart: 'Today',
|
|
73
|
-
metaEnd: 'Innsbruck',
|
|
74
75
|
isActive: true,
|
|
75
76
|
},
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
metaStart: 'Today',
|
|
84
|
-
metaEnd: 'Innsbruck',
|
|
85
|
-
},
|
|
77
|
+
{ title: 'Customer onboarding flow', metaStart: 'Yesterday' },
|
|
78
|
+
{ title: 'Refactor notes', metaStart: '2 days ago' },
|
|
79
|
+
{ title: 'Quarterly review prep', metaStart: '3 days ago' },
|
|
80
|
+
{ title: 'Sprint retro notes', metaStart: '4 days ago' },
|
|
81
|
+
{ title: 'Travel plans 2026', metaStart: '1 week ago' },
|
|
82
|
+
{ title: 'API design review', metaStart: '2 weeks ago' },
|
|
83
|
+
{ title: 'Bug triage session', metaStart: '3 weeks ago' },
|
|
86
84
|
];
|
|
87
85
|
|
|
88
86
|
function handleActionClick() {
|
|
@@ -99,7 +97,6 @@ export const Default: Story = {
|
|
|
99
97
|
:key="index"
|
|
100
98
|
:title="item.title"
|
|
101
99
|
:meta-start="item.metaStart"
|
|
102
|
-
:meta-end="item.metaEnd"
|
|
103
100
|
:is-active="item.isActive"
|
|
104
101
|
action-tooltip="More actions"
|
|
105
102
|
@action-click="handleActionClick"
|
|
@@ -128,7 +125,10 @@ export const WithoutGap: Story = {
|
|
|
128
125
|
const items = [
|
|
129
126
|
{ title: 'Thread A', metaStart: 'Today' },
|
|
130
127
|
{ title: 'Thread B', metaStart: 'Yesterday' },
|
|
131
|
-
{ title: 'Thread C', metaStart: '
|
|
128
|
+
{ title: 'Thread C', metaStart: '2 days ago' },
|
|
129
|
+
{ title: 'Thread D', metaStart: '3 days ago' },
|
|
130
|
+
{ title: 'Thread E', metaStart: '4 days ago' },
|
|
131
|
+
{ title: 'Thread F', metaStart: '1 week ago' },
|
|
132
132
|
];
|
|
133
133
|
return { args, items };
|
|
134
134
|
},
|
|
@@ -170,12 +170,119 @@ describe('CkHistory', () => {
|
|
|
170
170
|
expect(emitted().select).toBeUndefined();
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
it('renders a relative time label under each row by default', () => {
|
|
174
|
+
const conversations = [
|
|
175
|
+
makeConversation({ id: 'c-1', updatedAt: NOW - 60 * 60 * 24 }),
|
|
176
|
+
makeConversation({ id: 'c-2', updatedAt: NOW - 60 * 60 * 24 * 3 }),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
render(CkHistory, {
|
|
180
|
+
props: {
|
|
181
|
+
conversations,
|
|
182
|
+
hrefResolver,
|
|
183
|
+
timeFormatter: (s: number) => {
|
|
184
|
+
const days = Math.round((NOW - s) / (60 * 60 * 24));
|
|
185
|
+
return days === 1 ? 'Yesterday' : `${days} days ago`;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(screen.getByText('Yesterday')).toBeInTheDocument();
|
|
191
|
+
expect(screen.getByText('3 days ago')).toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('hides the time label when timeFormatter returns an empty string', () => {
|
|
195
|
+
const conversations = [makeConversation({ id: 'c-1', updatedAt: NOW })];
|
|
196
|
+
|
|
197
|
+
const { container } = render(CkHistory, {
|
|
198
|
+
props: {
|
|
199
|
+
conversations,
|
|
200
|
+
hrefResolver,
|
|
201
|
+
timeFormatter: () => '',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const metaElements = container.querySelectorAll('.label-12-medium');
|
|
206
|
+
expect(metaElements).toHaveLength(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('collapsed limit / Show all', () => {
|
|
210
|
+
const manyConversations = Array.from({ length: 8 }, (_, i) =>
|
|
211
|
+
makeConversation({ id: `c-${i + 1}`, title: `Thread ${i + 1}` }),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
it('shows only collapsedLimit rows and a "Show all" button when there are more conversations', () => {
|
|
215
|
+
render(CkHistory, {
|
|
216
|
+
props: {
|
|
217
|
+
conversations: manyConversations,
|
|
218
|
+
hrefResolver,
|
|
219
|
+
collapsedLimit: 5,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const links = screen.getAllByRole('link');
|
|
224
|
+
expect(links).toHaveLength(5);
|
|
225
|
+
expect(screen.getByTestId('ck-history-show-all')).toBeInTheDocument();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('does not show the "Show all" button when conversations fit within the limit', () => {
|
|
229
|
+
render(CkHistory, {
|
|
230
|
+
props: {
|
|
231
|
+
conversations: manyConversations.slice(0, 3),
|
|
232
|
+
hrefResolver,
|
|
233
|
+
collapsedLimit: 5,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(
|
|
238
|
+
screen.queryByTestId('ck-history-show-all'),
|
|
239
|
+
).not.toBeInTheDocument();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('reveals all conversations when "Show all" is clicked', async () => {
|
|
243
|
+
const user = userEvent.setup();
|
|
244
|
+
|
|
245
|
+
render(CkHistory, {
|
|
246
|
+
props: {
|
|
247
|
+
conversations: manyConversations,
|
|
248
|
+
hrefResolver,
|
|
249
|
+
collapsedLimit: 5,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(screen.getAllByRole('link')).toHaveLength(5);
|
|
254
|
+
|
|
255
|
+
await user.click(screen.getByTestId('ck-history-show-all'));
|
|
256
|
+
|
|
257
|
+
expect(screen.getAllByRole('link')).toHaveLength(8);
|
|
258
|
+
expect(
|
|
259
|
+
screen.queryByTestId('ck-history-show-all'),
|
|
260
|
+
).not.toBeInTheDocument();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('shows all conversations when collapsedLimit is 0', () => {
|
|
264
|
+
render(CkHistory, {
|
|
265
|
+
props: {
|
|
266
|
+
conversations: manyConversations,
|
|
267
|
+
hrefResolver,
|
|
268
|
+
collapsedLimit: 0,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(screen.getAllByRole('link')).toHaveLength(8);
|
|
273
|
+
expect(
|
|
274
|
+
screen.queryByTestId('ck-history-show-all'),
|
|
275
|
+
).not.toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
173
279
|
describe('#item slot', () => {
|
|
174
280
|
interface ItemSlotProps {
|
|
175
281
|
conversation: Conversation;
|
|
176
282
|
href: string;
|
|
177
283
|
isActive: boolean;
|
|
178
284
|
title: string;
|
|
285
|
+
time: string | undefined;
|
|
179
286
|
select: (event: MouseEvent) => void;
|
|
180
287
|
edit: () => void;
|
|
181
288
|
delete: () => void;
|
|
@@ -35,14 +35,38 @@ const sampleConversations: Conversation[] = [
|
|
|
35
35
|
id: 'c-5',
|
|
36
36
|
title: 'Quarterly review prep',
|
|
37
37
|
createdAt: NOW - ONE_DAY * 30,
|
|
38
|
-
updatedAt: NOW - ONE_DAY *
|
|
38
|
+
updatedAt: NOW - ONE_DAY * 3,
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
id: 'c-6',
|
|
42
42
|
title: 'Travel plans 2026',
|
|
43
43
|
createdAt: NOW - ONE_DAY * 60,
|
|
44
|
+
updatedAt: NOW - ONE_DAY * 4,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'c-7',
|
|
48
|
+
title: 'Sprint retro notes',
|
|
49
|
+
createdAt: NOW - ONE_DAY * 20,
|
|
50
|
+
updatedAt: NOW - ONE_DAY * 5,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'c-8',
|
|
54
|
+
title: 'API design review',
|
|
55
|
+
createdAt: NOW - ONE_DAY * 25,
|
|
56
|
+
updatedAt: NOW - ONE_DAY * 7,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'c-9',
|
|
60
|
+
title: 'Bug triage session',
|
|
61
|
+
createdAt: NOW - ONE_DAY * 40,
|
|
44
62
|
updatedAt: NOW - ONE_DAY * 14,
|
|
45
63
|
},
|
|
64
|
+
{
|
|
65
|
+
id: 'c-10',
|
|
66
|
+
title: 'Team standup summary',
|
|
67
|
+
createdAt: NOW - ONE_DAY * 50,
|
|
68
|
+
updatedAt: NOW - ONE_DAY * 21,
|
|
69
|
+
},
|
|
46
70
|
];
|
|
47
71
|
|
|
48
72
|
const defaultHrefResolver = (id: string) => `/chat/${id}`;
|
|
@@ -66,6 +90,8 @@ const meta: Meta<typeof CkHistory> = {
|
|
|
66
90
|
activeId: { control: 'text' },
|
|
67
91
|
showAction: { control: 'boolean' },
|
|
68
92
|
showActionOnHover: { control: 'boolean' },
|
|
93
|
+
collapsedLimit: { control: 'number' },
|
|
94
|
+
maxHeight: { control: 'text' },
|
|
69
95
|
},
|
|
70
96
|
};
|
|
71
97
|
|
|
@@ -184,7 +210,7 @@ function onDelete(c: Conversation) { /* confirm + delete */ }
|
|
|
184
210
|
@delete="onDelete"
|
|
185
211
|
>
|
|
186
212
|
<template
|
|
187
|
-
#item="{ conversation, isActive, title, edit, delete: del }"
|
|
213
|
+
#item="{ conversation, isActive, title, time, edit, delete: del }"
|
|
188
214
|
>
|
|
189
215
|
<RouterLink
|
|
190
216
|
:to="{ name: 'chat', params: { id: conversation.id } }"
|
|
@@ -196,6 +222,7 @@ function onDelete(c: Conversation) { /* confirm + delete */ }
|
|
|
196
222
|
:href="href"
|
|
197
223
|
:is-active="isActive"
|
|
198
224
|
:title="title"
|
|
225
|
+
:meta-start="time"
|
|
199
226
|
action-popover
|
|
200
227
|
@click="navigate"
|
|
201
228
|
>
|
|
@@ -209,6 +236,22 @@ function onDelete(c: Conversation) { /* confirm + delete */ }
|
|
|
209
236
|
</CkHistory>
|
|
210
237
|
</template>`;
|
|
211
238
|
|
|
239
|
+
const withoutTimeTemplateSource = `<script setup lang="ts">
|
|
240
|
+
import { CkHistory, type Conversation } from '@aleph-alpha/chat-kit';
|
|
241
|
+
|
|
242
|
+
const conversations: Conversation[] = [/* ... */];
|
|
243
|
+
const hrefResolver = (id: string) => \`/chat/\${id}\`;
|
|
244
|
+
</script>
|
|
245
|
+
|
|
246
|
+
<template>
|
|
247
|
+
<CkHistory
|
|
248
|
+
:conversations="conversations"
|
|
249
|
+
:href-resolver="hrefResolver"
|
|
250
|
+
active-id="c-2"
|
|
251
|
+
:time-formatter="undefined"
|
|
252
|
+
/>
|
|
253
|
+
</template>`;
|
|
254
|
+
|
|
212
255
|
const renderStory = (args: Story['args']) => ({
|
|
213
256
|
components: { CkHistory },
|
|
214
257
|
setup() {
|
|
@@ -329,13 +372,14 @@ export const WithCustomItem: Story = {
|
|
|
329
372
|
@select="onSelect"
|
|
330
373
|
>
|
|
331
374
|
<template
|
|
332
|
-
#item="{ href, isActive, title, select, edit, delete: del }"
|
|
375
|
+
#item="{ href, isActive, title, time, select, edit, delete: del }"
|
|
333
376
|
>
|
|
334
377
|
<CkHistoryItem
|
|
335
378
|
as="a"
|
|
336
379
|
:href="href"
|
|
337
380
|
:is-active="isActive"
|
|
338
381
|
:title="title"
|
|
382
|
+
:meta-start="time"
|
|
339
383
|
action-popover
|
|
340
384
|
action-aria-label="Conversation options"
|
|
341
385
|
@click="select($event)"
|
|
@@ -371,3 +415,47 @@ export const WithCustomItem: Story = {
|
|
|
371
415
|
docs: { source: { code: withCustomItemTemplateSource } },
|
|
372
416
|
},
|
|
373
417
|
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Hides the relative time label by setting `time-formatter` to `undefined`.
|
|
421
|
+
* Rows display only the title (the pre-v1.14 default).
|
|
422
|
+
*/
|
|
423
|
+
export const WithoutTime: Story = {
|
|
424
|
+
args: {
|
|
425
|
+
conversations: sampleConversations,
|
|
426
|
+
hrefResolver: defaultHrefResolver,
|
|
427
|
+
activeId: 'c-2',
|
|
428
|
+
timeFormatter: undefined,
|
|
429
|
+
},
|
|
430
|
+
render: renderStory,
|
|
431
|
+
parameters: { docs: { source: { code: withoutTimeTemplateSource } } },
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* With 10 conversations and a `collapsedLimit` of 5, only the first 5 rows
|
|
436
|
+
* are shown initially. A "Show all" button at the bottom reveals the rest
|
|
437
|
+
* and makes the list scrollable (capped at `maxHeight`).
|
|
438
|
+
*/
|
|
439
|
+
export const CollapsedWithShowAll: Story = {
|
|
440
|
+
args: {
|
|
441
|
+
conversations: sampleConversations,
|
|
442
|
+
hrefResolver: defaultHrefResolver,
|
|
443
|
+
activeId: 'c-2',
|
|
444
|
+
collapsedLimit: 5,
|
|
445
|
+
maxHeight: '24rem',
|
|
446
|
+
},
|
|
447
|
+
render: renderStory,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* All conversations visible without collapse — set `collapsedLimit` to `0`.
|
|
452
|
+
*/
|
|
453
|
+
export const NoCollapse: Story = {
|
|
454
|
+
args: {
|
|
455
|
+
conversations: sampleConversations,
|
|
456
|
+
hrefResolver: defaultHrefResolver,
|
|
457
|
+
activeId: 'c-2',
|
|
458
|
+
collapsedLimit: 0,
|
|
459
|
+
},
|
|
460
|
+
render: renderStory,
|
|
461
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
2
3
|
import {
|
|
3
4
|
UiIcon,
|
|
4
5
|
UiSidebarPopoverMenu,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
import CkHistoryItem from '../../base/CkHistoryItem/CkHistoryItem.vue';
|
|
8
9
|
import CkHistoryWrapper from '../../base/CkHistoryWrapper/CkHistoryWrapper.vue';
|
|
9
10
|
import { useChatKitLabels } from '../../../handlers/useChatKitLabels';
|
|
11
|
+
import { formatRelativeTime } from '../../../helpers/formatRelativeTime';
|
|
10
12
|
import type { Conversation } from '../../../types';
|
|
11
13
|
import type { CkHistoryEmits, CkHistoryProps } from './types';
|
|
12
14
|
|
|
@@ -16,6 +18,9 @@ const props = withDefaults(defineProps<CkHistoryProps>(), {
|
|
|
16
18
|
activeId: undefined,
|
|
17
19
|
showAction: true,
|
|
18
20
|
showActionOnHover: true,
|
|
21
|
+
timeFormatter: formatRelativeTime,
|
|
22
|
+
collapsedLimit: 5,
|
|
23
|
+
maxHeight: undefined,
|
|
19
24
|
});
|
|
20
25
|
|
|
21
26
|
const emit = defineEmits<CkHistoryEmits>();
|
|
@@ -31,6 +36,8 @@ defineSlots<{
|
|
|
31
36
|
* - `href` — URL produced by `hrefResolver(conversation.id)`
|
|
32
37
|
* - `isActive` — whether this row matches `activeId`
|
|
33
38
|
* - `title` — resolved title (falls back to `historyItemUntitled` label)
|
|
39
|
+
* - `time` — formatted relative time string from `timeFormatter`, or
|
|
40
|
+
* `undefined` when no formatter is set
|
|
34
41
|
* - `select(event)` — RouterLink-style click guard: `preventDefault()`
|
|
35
42
|
* and emit `select` for plain left-clicks; ignores middle/modifier
|
|
36
43
|
* clicks so native browser affordances still work.
|
|
@@ -41,6 +48,7 @@ defineSlots<{
|
|
|
41
48
|
href: string;
|
|
42
49
|
isActive: boolean;
|
|
43
50
|
title: string;
|
|
51
|
+
time: string | undefined;
|
|
44
52
|
select: (event: MouseEvent) => void;
|
|
45
53
|
edit: () => void;
|
|
46
54
|
delete: () => void;
|
|
@@ -49,6 +57,23 @@ defineSlots<{
|
|
|
49
57
|
|
|
50
58
|
const labels = useChatKitLabels();
|
|
51
59
|
|
|
60
|
+
const isExpanded = ref(false);
|
|
61
|
+
|
|
62
|
+
const canCollapse = computed(
|
|
63
|
+
() =>
|
|
64
|
+
props.collapsedLimit > 0 &&
|
|
65
|
+
props.conversations.length > props.collapsedLimit,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const visibleConversations = computed(() => {
|
|
69
|
+
if (!canCollapse.value || isExpanded.value) return props.conversations;
|
|
70
|
+
return props.conversations.slice(0, props.collapsedLimit);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function toggleExpanded() {
|
|
74
|
+
isExpanded.value = !isExpanded.value;
|
|
75
|
+
}
|
|
76
|
+
|
|
52
77
|
function hrefFor(c: Conversation) {
|
|
53
78
|
return props.hrefResolver(c.id);
|
|
54
79
|
}
|
|
@@ -58,6 +83,10 @@ function titleFor(c: Conversation) {
|
|
|
58
83
|
return title ? title : labels.value.historyItemUntitled;
|
|
59
84
|
}
|
|
60
85
|
|
|
86
|
+
function timeFor(c: Conversation): string | undefined {
|
|
87
|
+
return props.timeFormatter?.(c.updatedAt);
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
/**
|
|
62
91
|
* RouterLink-style click guard: only emit `select` for plain left-clicks.
|
|
63
92
|
* Middle-click, modifier-clicks, and right-click fall through to the
|
|
@@ -74,53 +103,74 @@ function handleRowClick(c: Conversation, event: MouseEvent) {
|
|
|
74
103
|
</script>
|
|
75
104
|
|
|
76
105
|
<template>
|
|
77
|
-
<
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
:
|
|
106
|
+
<div>
|
|
107
|
+
<CkHistoryWrapper
|
|
108
|
+
:class="isExpanded && props.maxHeight ? 'overflow-y-auto' : undefined"
|
|
109
|
+
:style="
|
|
110
|
+
isExpanded && props.maxHeight
|
|
111
|
+
? { maxHeight: props.maxHeight }
|
|
112
|
+
: undefined
|
|
113
|
+
"
|
|
81
114
|
>
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
:
|
|
85
|
-
:href="hrefFor(conversation)"
|
|
86
|
-
:is-active="conversation.id === props.activeId"
|
|
87
|
-
:title="titleFor(conversation)"
|
|
88
|
-
:select="(event: MouseEvent) => handleRowClick(conversation, event)"
|
|
89
|
-
:edit="() => emit('edit', conversation)"
|
|
90
|
-
:delete="() => emit('delete', conversation)"
|
|
115
|
+
<template
|
|
116
|
+
v-for="conversation in visibleConversations"
|
|
117
|
+
:key="conversation.id"
|
|
91
118
|
>
|
|
92
|
-
<
|
|
93
|
-
|
|
119
|
+
<slot
|
|
120
|
+
name="item"
|
|
121
|
+
:conversation="conversation"
|
|
94
122
|
:href="hrefFor(conversation)"
|
|
95
123
|
:is-active="conversation.id === props.activeId"
|
|
96
124
|
:title="titleFor(conversation)"
|
|
97
|
-
:
|
|
98
|
-
:
|
|
99
|
-
:
|
|
100
|
-
:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
:time="timeFor(conversation)"
|
|
126
|
+
:select="(event: MouseEvent) => handleRowClick(conversation, event)"
|
|
127
|
+
:edit="() => emit('edit', conversation)"
|
|
128
|
+
:delete="() => emit('delete', conversation)"
|
|
129
|
+
>
|
|
130
|
+
<CkHistoryItem
|
|
131
|
+
as="a"
|
|
132
|
+
:href="hrefFor(conversation)"
|
|
133
|
+
:is-active="conversation.id === props.activeId"
|
|
134
|
+
:title="titleFor(conversation)"
|
|
135
|
+
:meta-start="timeFor(conversation)"
|
|
136
|
+
:show-action="props.showAction"
|
|
137
|
+
:show-action-on-hover="props.showActionOnHover"
|
|
138
|
+
:action-popover="props.showAction"
|
|
139
|
+
:action-aria-label="labels.historyItemActionAriaLabel"
|
|
140
|
+
@click="handleRowClick(conversation, $event)"
|
|
141
|
+
><template #action-popover>
|
|
142
|
+
<UiSidebarPopoverMenu :show-footer-separator="false">
|
|
143
|
+
<UiSidebarPopoverMenuItem
|
|
144
|
+
:label="labels.historyItemEditAction"
|
|
145
|
+
@click="emit('edit', conversation)"
|
|
146
|
+
>
|
|
147
|
+
<template #icon>
|
|
148
|
+
<UiIcon name="pencil" size="s" />
|
|
149
|
+
</template>
|
|
150
|
+
</UiSidebarPopoverMenuItem>
|
|
151
|
+
<UiSidebarPopoverMenuItem
|
|
152
|
+
:label="labels.historyItemDeleteAction"
|
|
153
|
+
@click="emit('delete', conversation)"
|
|
154
|
+
>
|
|
155
|
+
<template #icon>
|
|
156
|
+
<UiIcon name="trash" size="s" />
|
|
157
|
+
</template>
|
|
158
|
+
</UiSidebarPopoverMenuItem>
|
|
159
|
+
</UiSidebarPopoverMenu>
|
|
160
|
+
</template>
|
|
161
|
+
</CkHistoryItem>
|
|
162
|
+
</slot>
|
|
163
|
+
</template>
|
|
164
|
+
</CkHistoryWrapper>
|
|
165
|
+
|
|
166
|
+
<button
|
|
167
|
+
v-if="canCollapse && !isExpanded"
|
|
168
|
+
type="button"
|
|
169
|
+
data-testid="ck-history-show-all"
|
|
170
|
+
class="mt-1 w-full rounded-lg px-2 py-2 text-center body-14-medium text-content-on-surface-muted transition-colors hover:bg-background-surface-muted-subtle"
|
|
171
|
+
@click="toggleExpanded"
|
|
172
|
+
>
|
|
173
|
+
{{ labels.historyShowAll }}
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
126
176
|
</template>
|
|
@@ -50,6 +50,39 @@ export interface CkHistoryProps {
|
|
|
50
50
|
* @default true
|
|
51
51
|
*/
|
|
52
52
|
showActionOnHover?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Formats a conversation's `updatedAt` timestamp (Unix seconds) into a
|
|
55
|
+
* human-readable relative time string displayed under the title.
|
|
56
|
+
*
|
|
57
|
+
* Defaults to the built-in `formatRelativeTime` which produces English
|
|
58
|
+
* labels like "Just now", "Yesterday", "3 days ago". Pass your own
|
|
59
|
+
* function for locale-aware formatting (e.g. via `Intl.RelativeTimeFormat`
|
|
60
|
+
* or your i18n library).
|
|
61
|
+
*
|
|
62
|
+
* To hide the time label entirely, pass `() => ''`.
|
|
63
|
+
*
|
|
64
|
+
* @default formatRelativeTime
|
|
65
|
+
*/
|
|
66
|
+
timeFormatter?: (updatedAtSeconds: number) => string;
|
|
67
|
+
/**
|
|
68
|
+
* Maximum number of conversations shown in collapsed mode. When the
|
|
69
|
+
* array has more items than this limit a "Show all" button is rendered
|
|
70
|
+
* below the visible rows. Clicking it expands the list and makes it
|
|
71
|
+
* scrollable (capped at `maxHeight`).
|
|
72
|
+
*
|
|
73
|
+
* Set to `0` or `undefined` to disable collapsing — all conversations
|
|
74
|
+
* are shown and the list is always scrollable.
|
|
75
|
+
*
|
|
76
|
+
* @default 5
|
|
77
|
+
*/
|
|
78
|
+
collapsedLimit?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Maximum height (CSS value) of the list when expanded. Allows the list
|
|
81
|
+
* to scroll when conversations exceed the available space.
|
|
82
|
+
*
|
|
83
|
+
* @default undefined — no max-height constraint
|
|
84
|
+
*/
|
|
85
|
+
maxHeight?: string;
|
|
53
86
|
}
|
|
54
87
|
|
|
55
88
|
/**
|
|
@@ -60,6 +60,7 @@ describe('useChatKitLabels', () => {
|
|
|
60
60
|
historyItemActionAriaLabel: 'Konversationsoptionen',
|
|
61
61
|
historyItemEditAction: 'Bearbeiten',
|
|
62
62
|
historyItemDeleteAction: 'Löschen',
|
|
63
|
+
historyShowAll: 'Alle anzeigen',
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
let result: ComputedRef<ChatKitLabels> | undefined;
|
|
@@ -77,6 +77,11 @@ export type ChatKitLabels = {
|
|
|
77
77
|
historyItemEditAction: string;
|
|
78
78
|
/** Label for the "Delete" action in the `CkHistory` row popover menu. */
|
|
79
79
|
historyItemDeleteAction: string;
|
|
80
|
+
/**
|
|
81
|
+
* Label for the "Show all" button that appears when `collapsedLimit` is
|
|
82
|
+
* set and more conversations exist than the limit.
|
|
83
|
+
*/
|
|
84
|
+
historyShowAll?: string;
|
|
80
85
|
};
|
|
81
86
|
|
|
82
87
|
/** English defaults used when no labels are provided by the consumer. */
|
|
@@ -106,6 +111,7 @@ export const defaultChatKitLabels: ChatKitLabels = {
|
|
|
106
111
|
historyItemActionAriaLabel: 'Conversation options',
|
|
107
112
|
historyItemEditAction: 'Edit',
|
|
108
113
|
historyItemDeleteAction: 'Delete',
|
|
114
|
+
historyShowAll: 'Show all',
|
|
109
115
|
};
|
|
110
116
|
|
|
111
117
|
/** Vue injection key for ChatKit labels. */
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const MINUTE = 60;
|
|
2
|
+
const HOUR = 60 * MINUTE;
|
|
3
|
+
const DAY = 24 * HOUR;
|
|
4
|
+
const WEEK = 7 * DAY;
|
|
5
|
+
const MONTH = 30 * DAY;
|
|
6
|
+
const YEAR = 365 * DAY;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert a Unix-seconds timestamp to a human-readable relative time string.
|
|
10
|
+
*
|
|
11
|
+
* Uses simple threshold-based bucketing (not `Intl.RelativeTimeFormat`) so the
|
|
12
|
+
* output is deterministic and easy to snapshot-test. Consumers who need
|
|
13
|
+
* locale-aware formatting can supply their own formatter via the
|
|
14
|
+
* `timeFormatter` prop on `CkHistory`.
|
|
15
|
+
*/
|
|
16
|
+
export function formatRelativeTime(
|
|
17
|
+
unixSeconds: number,
|
|
18
|
+
nowSeconds: number = Math.floor(Date.now() / 1000),
|
|
19
|
+
): string {
|
|
20
|
+
const delta = Math.max(0, nowSeconds - unixSeconds);
|
|
21
|
+
|
|
22
|
+
if (delta < MINUTE) return 'Just now';
|
|
23
|
+
if (delta < HOUR) {
|
|
24
|
+
const mins = Math.floor(delta / MINUTE);
|
|
25
|
+
return mins === 1 ? '1 minute ago' : `${mins} minutes ago`;
|
|
26
|
+
}
|
|
27
|
+
if (delta < DAY) {
|
|
28
|
+
const hrs = Math.floor(delta / HOUR);
|
|
29
|
+
return hrs === 1 ? '1 hour ago' : `${hrs} hours ago`;
|
|
30
|
+
}
|
|
31
|
+
if (delta < 2 * DAY) return 'Yesterday';
|
|
32
|
+
if (delta < WEEK) {
|
|
33
|
+
const days = Math.floor(delta / DAY);
|
|
34
|
+
return `${days} days ago`;
|
|
35
|
+
}
|
|
36
|
+
if (delta < 2 * WEEK) return '1 week ago';
|
|
37
|
+
if (delta < MONTH) {
|
|
38
|
+
const weeks = Math.floor(delta / WEEK);
|
|
39
|
+
return `${weeks} weeks ago`;
|
|
40
|
+
}
|
|
41
|
+
if (delta < 2 * MONTH) return '1 month ago';
|
|
42
|
+
if (delta < YEAR) {
|
|
43
|
+
const months = Math.floor(delta / MONTH);
|
|
44
|
+
return `${months} months ago`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const years = Math.floor(delta / YEAR);
|
|
48
|
+
return years === 1 ? '1 year ago' : `${years} years ago`;
|
|
49
|
+
}
|
package/src/helpers/index.ts
CHANGED