@aleph-alpha/chat-kit 1.14.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.
Files changed (40) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-BVZ7FG88.js → CkInput.vue_vue_type_script_setup_true_lang-DedLzDoO.js} +157 -82
  2. package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts.map +1 -1
  3. package/dist/components/base/CkHistoryItem/CkHistoryItem.stories.d.ts.map +1 -1
  4. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
  5. package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.d.ts.map +1 -1
  6. package/dist/components/composed/CkHistory/CkHistory.stories.d.ts +15 -0
  7. package/dist/components/composed/CkHistory/CkHistory.stories.d.ts.map +1 -1
  8. package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +6 -0
  9. package/dist/components/composed/CkHistory/CkHistory.vue.d.ts.map +1 -1
  10. package/dist/components/composed/CkHistory/types.d.ts +33 -0
  11. package/dist/components/composed/CkHistory/types.d.ts.map +1 -1
  12. package/dist/components/index.js +1 -1
  13. package/dist/handlers/index.js +1 -1
  14. package/dist/handlers/useChatKitLabels.d.ts +5 -0
  15. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  16. package/dist/helpers/formatRelativeTime.d.ts +10 -0
  17. package/dist/helpers/formatRelativeTime.d.ts.map +1 -0
  18. package/dist/helpers/index.d.ts +1 -0
  19. package/dist/helpers/index.d.ts.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -2
  23. package/dist/{useChatKitLabels-BxNkTzr2.js → useChatKitLabels-BC6ItJc6.js} +2 -1
  24. package/package.json +1 -1
  25. package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +6 -12
  26. package/src/components/base/CkAgentMessage/CkAgentMessage.vue +24 -15
  27. package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +0 -2
  28. package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +0 -6
  29. package/src/components/base/CkHistoryItem/CkHistoryItem.vue +2 -3
  30. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +23 -23
  31. package/src/components/composed/CkConversation/CkConversation.spec.ts +8 -35
  32. package/src/components/composed/CkHistory/CkHistory.spec.ts +107 -0
  33. package/src/components/composed/CkHistory/CkHistory.stories.ts +91 -3
  34. package/src/components/composed/CkHistory/CkHistory.vue +94 -44
  35. package/src/components/composed/CkHistory/types.ts +33 -0
  36. package/src/handlers/useChatKitLabels.spec.ts +1 -0
  37. package/src/handlers/useChatKitLabels.ts +6 -0
  38. package/src/helpers/formatRelativeTime.ts +49 -0
  39. package/src/helpers/index.ts +1 -0
  40. package/src/index.ts +3 -0
@@ -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
- <CkHistoryWrapper>
78
- <template
79
- v-for="conversation in props.conversations"
80
- :key="conversation.id"
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
- <slot
83
- name="item"
84
- :conversation="conversation"
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
- <CkHistoryItem
93
- as="a"
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
- :show-action="props.showAction"
98
- :show-action-on-hover="props.showActionOnHover"
99
- :action-popover="props.showAction"
100
- :action-aria-label="labels.historyItemActionAriaLabel"
101
- @click="handleRowClick(conversation, $event)"
102
- ><template #action-popover>
103
- <UiSidebarPopoverMenu :show-footer-separator="false">
104
- <UiSidebarPopoverMenuItem
105
- :label="labels.historyItemEditAction"
106
- @click="emit('edit', conversation)"
107
- >
108
- <template #icon>
109
- <UiIcon name="pencil" size="s" />
110
- </template>
111
- </UiSidebarPopoverMenuItem>
112
- <UiSidebarPopoverMenuItem
113
- :label="labels.historyItemDeleteAction"
114
- @click="emit('delete', conversation)"
115
- >
116
- <template #icon>
117
- <UiIcon name="trash" size="s" />
118
- </template>
119
- </UiSidebarPopoverMenuItem>
120
- </UiSidebarPopoverMenu>
121
- </template>
122
- </CkHistoryItem>
123
- </slot>
124
- </template>
125
- </CkHistoryWrapper>
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
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './conversationTree';
2
+ export * from './formatRelativeTime';
2
3
  export * from './generateId';
3
4
  export * from './secondsToDate';
4
5
  export * from './toError';
package/src/index.ts CHANGED
@@ -106,6 +106,9 @@ export type {
106
106
  CkUserMessageProps,
107
107
  } from './components';
108
108
 
109
+ // Helpers
110
+ export { formatRelativeTime } from './helpers';
111
+
109
112
  // Shared Types
110
113
  export type {
111
114
  BaseMessage,