@juspay/svelte-ui-components 2.124.0 → 2.125.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/Chat/Chat.svelte +158 -0
- package/dist/Chat/Chat.svelte.d.ts +4 -0
- package/dist/Chat/controller.svelte.d.ts +24 -0
- package/dist/Chat/controller.svelte.js +190 -0
- package/dist/Chat/properties.d.ts +51 -0
- package/dist/Chat/properties.js +1 -0
- package/dist/Chat/roles.d.ts +2 -0
- package/dist/Chat/roles.js +3 -0
- package/dist/Chat/types.d.ts +45 -0
- package/dist/Chat/types.js +1 -0
- package/dist/ChatBubble/ChatBubble.svelte +419 -0
- package/dist/ChatBubble/ChatBubble.svelte.d.ts +4 -0
- package/dist/ChatBubble/properties.d.ts +43 -0
- package/dist/ChatBubble/properties.js +1 -0
- package/dist/ChatComposer/ChatComposer.svelte +289 -0
- package/dist/ChatComposer/ChatComposer.svelte.d.ts +4 -0
- package/dist/ChatComposer/properties.d.ts +33 -0
- package/dist/ChatComposer/properties.js +1 -0
- package/dist/ChatHeader/ChatHeader.svelte +169 -0
- package/dist/ChatHeader/ChatHeader.svelte.d.ts +4 -0
- package/dist/ChatHeader/properties.d.ts +19 -0
- package/dist/ChatHeader/properties.js +1 -0
- package/dist/ChatMessage/ChatMessage.svelte +330 -0
- package/dist/ChatMessage/ChatMessage.svelte.d.ts +4 -0
- package/dist/ChatMessage/properties.d.ts +29 -0
- package/dist/ChatMessage/properties.js +1 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +181 -0
- package/dist/ChatMessageList/ChatMessageList.svelte.d.ts +4 -0
- package/dist/ChatMessageList/properties.d.ts +22 -0
- package/dist/ChatMessageList/properties.js +1 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte +46 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte.d.ts +4 -0
- package/dist/ChatSuggestions/properties.d.ts +16 -0
- package/dist/ChatSuggestions/properties.js +1 -0
- package/dist/ChatToolStatus/ChatToolStatus.svelte +63 -0
- package/dist/ChatToolStatus/ChatToolStatus.svelte.d.ts +4 -0
- package/dist/ChatToolStatus/properties.d.ts +10 -0
- package/dist/ChatToolStatus/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +36 -15
- package/dist/Resizable/Resizable.svelte +304 -0
- package/dist/Resizable/Resizable.svelte.d.ts +4 -0
- package/dist/Resizable/properties.d.ts +27 -0
- package/dist/Resizable/properties.js +1 -0
- package/dist/assets/attach.svg +3 -0
- package/dist/assets/chat.svg +3 -0
- package/dist/assets/mic.svg +5 -0
- package/dist/assets/retry.svg +4 -0
- package/dist/assets/send.svg +4 -0
- package/dist/assets/stop.svg +3 -0
- package/dist/assets/thumb-down.svg +4 -0
- package/dist/assets/thumb-up.svg +4 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../Button/Button.svelte';
|
|
3
|
+
import LoadingDots from '../LoadingDots/LoadingDots.svelte';
|
|
4
|
+
import copySvg from '../assets/copy.svg?raw';
|
|
5
|
+
import checkmarkSvg from '../assets/checkmark.svg?raw';
|
|
6
|
+
import retrySvg from '../assets/retry.svg?raw';
|
|
7
|
+
import thumbUpSvg from '../assets/thumb-up.svg?raw';
|
|
8
|
+
import thumbDownSvg from '../assets/thumb-down.svg?raw';
|
|
9
|
+
import { partyOf } from '../Chat/roles';
|
|
10
|
+
import type { ChatMessageProperties } from './properties';
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
role,
|
|
14
|
+
content = '',
|
|
15
|
+
html,
|
|
16
|
+
streaming = false,
|
|
17
|
+
status,
|
|
18
|
+
avatar,
|
|
19
|
+
header,
|
|
20
|
+
attachments,
|
|
21
|
+
allowCopy = false,
|
|
22
|
+
actions,
|
|
23
|
+
copyLabel = 'Copy',
|
|
24
|
+
retryLabel = 'Retry',
|
|
25
|
+
feedbackUpLabel = 'Good response',
|
|
26
|
+
feedbackDownLabel = 'Bad response',
|
|
27
|
+
onretry,
|
|
28
|
+
onfeedback,
|
|
29
|
+
oncopy,
|
|
30
|
+
testId,
|
|
31
|
+
classes
|
|
32
|
+
}: ChatMessageProperties = $props();
|
|
33
|
+
|
|
34
|
+
let party = $derived(partyOf(role));
|
|
35
|
+
|
|
36
|
+
let copied = $state(false);
|
|
37
|
+
let copyResetTimer: ReturnType<typeof setTimeout> | null = null;
|
|
38
|
+
|
|
39
|
+
let hasHtml = $derived(typeof html === 'string' && html.length > 0);
|
|
40
|
+
let hasContent = $derived(hasHtml || content.length > 0);
|
|
41
|
+
let showTyping = $derived(streaming && !hasContent);
|
|
42
|
+
let showRetry = $derived(typeof onretry === 'function');
|
|
43
|
+
let showFeedback = $derived(typeof onfeedback === 'function');
|
|
44
|
+
let showActions = $derived(
|
|
45
|
+
!streaming && (allowCopy || showRetry || showFeedback || typeof actions === 'function')
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function htmlToText(source: string): string {
|
|
49
|
+
if (typeof DOMParser === 'undefined') {
|
|
50
|
+
return source;
|
|
51
|
+
}
|
|
52
|
+
return new DOMParser().parseFromString(source, 'text/html').body.textContent ?? '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function copy(): Promise<void> {
|
|
56
|
+
const text = content.length > 0 ? content : typeof html === 'string' ? htmlToText(html) : '';
|
|
57
|
+
if (typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function') {
|
|
58
|
+
try {
|
|
59
|
+
await navigator.clipboard.writeText(text);
|
|
60
|
+
copied = true;
|
|
61
|
+
if (copyResetTimer !== null) {
|
|
62
|
+
clearTimeout(copyResetTimer);
|
|
63
|
+
}
|
|
64
|
+
copyResetTimer = setTimeout(() => {
|
|
65
|
+
copied = false;
|
|
66
|
+
}, 1500);
|
|
67
|
+
} catch {
|
|
68
|
+
copied = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
oncopy?.(text);
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<article
|
|
76
|
+
class="chat-message party-{party} role-{role} {classes ?? ''}"
|
|
77
|
+
class:streaming
|
|
78
|
+
class:error={status === 'error'}
|
|
79
|
+
aria-label={party === 'sender' ? 'Your message' : 'Message'}
|
|
80
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
81
|
+
testID={typeof testId === 'string' ? testId : null}
|
|
82
|
+
>
|
|
83
|
+
<div class="row">
|
|
84
|
+
{#if typeof avatar === 'function'}
|
|
85
|
+
<div class="avatar">{@render avatar()}</div>
|
|
86
|
+
{/if}
|
|
87
|
+
|
|
88
|
+
<div class="content">
|
|
89
|
+
{#if typeof header === 'function'}
|
|
90
|
+
<div class="header">{@render header()}</div>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
93
|
+
<div class="bubble">
|
|
94
|
+
{#if hasHtml}
|
|
95
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
96
|
+
<div class="body">{@html html}</div>
|
|
97
|
+
{:else if content.length > 0}
|
|
98
|
+
<div class="body text">{content}</div>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
{#if showTyping}
|
|
102
|
+
<span class="typing"><LoadingDots /></span>
|
|
103
|
+
{/if}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{#if typeof attachments === 'function'}
|
|
107
|
+
<div class="message-attachments">{@render attachments()}</div>
|
|
108
|
+
{/if}
|
|
109
|
+
|
|
110
|
+
{#if showActions}
|
|
111
|
+
<div class="actions">
|
|
112
|
+
{#if allowCopy}
|
|
113
|
+
<div class="action">
|
|
114
|
+
<Button onclick={copy} ariaLabel={copyLabel}>
|
|
115
|
+
{#if copied}
|
|
116
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
117
|
+
{@html checkmarkSvg}
|
|
118
|
+
{:else}
|
|
119
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
120
|
+
{@html copySvg}
|
|
121
|
+
{/if}
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
124
|
+
{/if}
|
|
125
|
+
{#if showRetry}
|
|
126
|
+
<div class="action">
|
|
127
|
+
<Button onclick={() => onretry?.()} ariaLabel={retryLabel}>
|
|
128
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
129
|
+
{@html retrySvg}
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
{/if}
|
|
133
|
+
{#if showFeedback}
|
|
134
|
+
<div class="action">
|
|
135
|
+
<Button onclick={() => onfeedback?.('up')} ariaLabel={feedbackUpLabel}>
|
|
136
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
137
|
+
{@html thumbUpSvg}
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="action">
|
|
141
|
+
<Button onclick={() => onfeedback?.('down')} ariaLabel={feedbackDownLabel}>
|
|
142
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
143
|
+
{@html thumbDownSvg}
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
{/if}
|
|
147
|
+
{#if typeof actions === 'function'}
|
|
148
|
+
{@render actions()}
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
151
|
+
{/if}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</article>
|
|
155
|
+
|
|
156
|
+
<style>
|
|
157
|
+
.chat-message {
|
|
158
|
+
box-sizing: border-box;
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
max-width: var(--chat-message-max-width, 82%);
|
|
162
|
+
margin: var(--chat-message-margin, 0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.chat-message.party-sender {
|
|
166
|
+
align-self: flex-end;
|
|
167
|
+
align-items: flex-end;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.chat-message.party-responder {
|
|
171
|
+
align-self: flex-start;
|
|
172
|
+
align-items: flex-start;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.row {
|
|
176
|
+
display: flex;
|
|
177
|
+
gap: var(--chat-message-gap, 10px);
|
|
178
|
+
align-items: flex-end;
|
|
179
|
+
width: 100%;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.chat-message.party-sender .row {
|
|
183
|
+
flex-direction: row-reverse;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.avatar {
|
|
187
|
+
display: flex;
|
|
188
|
+
flex-shrink: 0;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: center;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.content {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
gap: var(--chat-message-content-gap, 6px);
|
|
197
|
+
min-width: 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.header {
|
|
201
|
+
font-size: var(--chat-message-header-font-size, 0.75rem);
|
|
202
|
+
color: var(--chat-message-header-color, #71717a);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.bubble {
|
|
206
|
+
box-sizing: border-box;
|
|
207
|
+
padding: var(--chat-message-bubble-padding, 9px 13px);
|
|
208
|
+
border-radius: var(--chat-message-bubble-border-radius, 16px);
|
|
209
|
+
font-size: var(--chat-message-font-size, 0.9375rem);
|
|
210
|
+
line-height: var(--chat-message-line-height, 1.5);
|
|
211
|
+
color: var(--chat-message-color, #27272a);
|
|
212
|
+
background: var(--chat-message-background, #f4f4f5);
|
|
213
|
+
border: var(--chat-message-border, none);
|
|
214
|
+
box-shadow: var(--chat-message-box-shadow, none);
|
|
215
|
+
word-wrap: break-word;
|
|
216
|
+
overflow-wrap: anywhere;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.chat-message.party-sender .bubble {
|
|
220
|
+
color: var(--chat-message-sender-color, #ffffff);
|
|
221
|
+
background: var(--chat-message-sender-background, #18181b);
|
|
222
|
+
border: var(--chat-message-sender-border, none);
|
|
223
|
+
border-radius: var(
|
|
224
|
+
--chat-message-sender-border-radius,
|
|
225
|
+
var(--chat-message-bubble-border-radius, 16px)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.chat-message.party-responder .bubble {
|
|
230
|
+
color: var(--chat-message-responder-color, var(--chat-message-color, #27272a));
|
|
231
|
+
background: var(--chat-message-responder-background, transparent);
|
|
232
|
+
border: var(--chat-message-responder-border, none);
|
|
233
|
+
padding: var(--chat-message-responder-padding, 2px 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.chat-message.error .bubble {
|
|
237
|
+
color: var(--chat-message-error-color, #e0334b);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.text {
|
|
241
|
+
white-space: pre-wrap;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.typing {
|
|
245
|
+
display: inline-flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.message-attachments {
|
|
250
|
+
display: flex;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
gap: var(--chat-message-attachments-gap, 8px);
|
|
253
|
+
margin: var(--chat-message-attachments-margin, 4px 0 0 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.message-attachments:empty {
|
|
257
|
+
display: none;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.actions {
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: var(--chat-message-actions-gap, 2px);
|
|
264
|
+
opacity: var(--chat-message-actions-opacity, 0);
|
|
265
|
+
transition: var(--chat-message-actions-transition, opacity 0.15s ease);
|
|
266
|
+
--button-width: var(--chat-message-action-size, 28px);
|
|
267
|
+
--button-height: var(--chat-message-action-size, 28px);
|
|
268
|
+
--button-padding: var(--chat-message-action-padding, 6px);
|
|
269
|
+
--button-border-radius: var(--chat-message-action-border-radius, 6px);
|
|
270
|
+
--button-color: var(--chat-message-action-background-color, transparent);
|
|
271
|
+
--button-text-color: var(--chat-message-action-color, #71717a);
|
|
272
|
+
--button-content-gap: 0px;
|
|
273
|
+
--button-hover-color: var(--chat-message-action-hover-background-color, #f4f4f5);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.chat-message:hover .actions,
|
|
277
|
+
.chat-message:focus-within .actions {
|
|
278
|
+
opacity: 1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.action {
|
|
282
|
+
display: flex;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.action :global(svg) {
|
|
286
|
+
height: 100%;
|
|
287
|
+
width: 100%;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@media (hover: none) {
|
|
291
|
+
.actions {
|
|
292
|
+
opacity: 1;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.body :global(p) {
|
|
297
|
+
margin: var(--chat-message-paragraph-margin, 0 0 0.5em 0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.body :global(p:last-child) {
|
|
301
|
+
margin-bottom: 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.body :global(a) {
|
|
305
|
+
color: var(--chat-message-link-color, #6d28d9);
|
|
306
|
+
text-decoration: underline;
|
|
307
|
+
text-underline-offset: 2px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.body :global(code) {
|
|
311
|
+
font-family: var(--chat-message-code-font-family, ui-monospace, monospace);
|
|
312
|
+
font-size: 0.88em;
|
|
313
|
+
background: var(--chat-message-code-background, rgba(0, 0, 0, 0.05));
|
|
314
|
+
padding: 1px 5px;
|
|
315
|
+
border-radius: 4px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.body :global(pre) {
|
|
319
|
+
background: var(--chat-message-pre-background, rgba(0, 0, 0, 0.05));
|
|
320
|
+
padding: 10px 12px;
|
|
321
|
+
border-radius: 8px;
|
|
322
|
+
overflow-x: auto;
|
|
323
|
+
margin: 0.5em 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.body :global(pre code) {
|
|
327
|
+
background: transparent;
|
|
328
|
+
padding: 0;
|
|
329
|
+
}
|
|
330
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ChatMessageStatus, ChatRole } from '../Chat/types';
|
|
3
|
+
export type ChatMessageFeedback = 'up' | 'down';
|
|
4
|
+
export type ChatMessageProperties = OptionalChatMessageProperties & ChatMessageEventProperties & MandatoryChatMessageProperties;
|
|
5
|
+
export type MandatoryChatMessageProperties = {
|
|
6
|
+
role: ChatRole;
|
|
7
|
+
};
|
|
8
|
+
export type OptionalChatMessageProperties = {
|
|
9
|
+
content?: string;
|
|
10
|
+
html?: string;
|
|
11
|
+
streaming?: boolean;
|
|
12
|
+
status?: ChatMessageStatus;
|
|
13
|
+
avatar?: Snippet;
|
|
14
|
+
header?: Snippet;
|
|
15
|
+
attachments?: Snippet | null;
|
|
16
|
+
allowCopy?: boolean;
|
|
17
|
+
actions?: Snippet;
|
|
18
|
+
copyLabel?: string;
|
|
19
|
+
retryLabel?: string;
|
|
20
|
+
feedbackUpLabel?: string;
|
|
21
|
+
feedbackDownLabel?: string;
|
|
22
|
+
testId?: string;
|
|
23
|
+
classes?: string;
|
|
24
|
+
};
|
|
25
|
+
export type ChatMessageEventProperties = {
|
|
26
|
+
onretry?: (() => void) | null;
|
|
27
|
+
onfeedback?: ((value: ChatMessageFeedback) => void) | null;
|
|
28
|
+
oncopy?: (text: string) => void;
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../Button/Button.svelte';
|
|
3
|
+
import ChatMessage from '../ChatMessage/ChatMessage.svelte';
|
|
4
|
+
import chevronDownSvg from '../assets/chevron-down.svg?raw';
|
|
5
|
+
import { partyOf } from '../Chat/roles';
|
|
6
|
+
import type { Action } from 'svelte/action';
|
|
7
|
+
import type { ChatMessageData } from '../Chat/types';
|
|
8
|
+
import type { ChatMessageFeedback } from '../ChatMessage/properties';
|
|
9
|
+
import type { ChatMessageListProperties } from './properties';
|
|
10
|
+
|
|
11
|
+
const NEAR_BOTTOM_THRESHOLD = 80;
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
messages,
|
|
15
|
+
autoscroll = true,
|
|
16
|
+
message,
|
|
17
|
+
messageAttachments,
|
|
18
|
+
empty,
|
|
19
|
+
jumpLabel = 'Jump to latest',
|
|
20
|
+
jumpIcon,
|
|
21
|
+
allowCopy = false,
|
|
22
|
+
onretry,
|
|
23
|
+
onfeedback,
|
|
24
|
+
testId,
|
|
25
|
+
classes
|
|
26
|
+
}: ChatMessageListProperties = $props();
|
|
27
|
+
|
|
28
|
+
let listEl: HTMLElement | null = $state(null);
|
|
29
|
+
let atBottom = $state(true);
|
|
30
|
+
|
|
31
|
+
let scrollKey = $derived(`${messages.length}:${messages.at(-1)?.content.length ?? 0}`);
|
|
32
|
+
let showJump = $derived(!atBottom && messages.length > 0);
|
|
33
|
+
let lastResponderId = $derived.by(() => {
|
|
34
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
35
|
+
const candidate = messages.at(i);
|
|
36
|
+
if (candidate && partyOf(candidate.role) === 'responder') {
|
|
37
|
+
return candidate.id;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function retryFor(msg: ChatMessageData): (() => void) | null {
|
|
44
|
+
if (
|
|
45
|
+
typeof onretry === 'function' &&
|
|
46
|
+
partyOf(msg.role) === 'responder' &&
|
|
47
|
+
msg.id === lastResponderId
|
|
48
|
+
) {
|
|
49
|
+
return onretry;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function feedbackFor(msg: ChatMessageData): ((value: ChatMessageFeedback) => void) | null {
|
|
55
|
+
if (typeof onfeedback === 'function' && partyOf(msg.role) === 'responder') {
|
|
56
|
+
return (value) => onfeedback?.(value, msg);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isNearBottom(node: HTMLElement): boolean {
|
|
62
|
+
return node.scrollHeight - node.scrollTop - node.clientHeight < NEAR_BOTTOM_THRESHOLD;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleScroll(event: Event & { currentTarget: HTMLElement }): void {
|
|
66
|
+
atBottom = isNearBottom(event.currentTarget);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function scrollToBottom(): void {
|
|
70
|
+
if (listEl !== null) {
|
|
71
|
+
listEl.scrollTop = listEl.scrollHeight;
|
|
72
|
+
atBottom = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pinToBottom: Action<HTMLElement, string> = (node) => {
|
|
77
|
+
let previousCount = messages.length;
|
|
78
|
+
function scroll(): void {
|
|
79
|
+
const newMessage = messages.length > previousCount;
|
|
80
|
+
previousCount = messages.length;
|
|
81
|
+
if (autoscroll && (atBottom || newMessage)) {
|
|
82
|
+
queueMicrotask(() => {
|
|
83
|
+
node.scrollTop = node.scrollHeight;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
scroll();
|
|
88
|
+
return { update: scroll };
|
|
89
|
+
};
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
class="chat-message-list {classes ?? ''}"
|
|
94
|
+
role="log"
|
|
95
|
+
aria-live="polite"
|
|
96
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
97
|
+
testID={typeof testId === 'string' ? testId : null}
|
|
98
|
+
bind:this={listEl}
|
|
99
|
+
onscroll={handleScroll}
|
|
100
|
+
use:pinToBottom={scrollKey}
|
|
101
|
+
>
|
|
102
|
+
{#if messages.length === 0 && typeof empty === 'function'}
|
|
103
|
+
{@render empty()}
|
|
104
|
+
{/if}
|
|
105
|
+
|
|
106
|
+
{#each messages as msg (msg.id)}
|
|
107
|
+
{#if typeof message === 'function'}
|
|
108
|
+
{@render message(msg)}
|
|
109
|
+
{:else}
|
|
110
|
+
{#snippet attachmentsFor()}
|
|
111
|
+
{@render messageAttachments?.(msg)}
|
|
112
|
+
{/snippet}
|
|
113
|
+
<ChatMessage
|
|
114
|
+
role={msg.role}
|
|
115
|
+
content={msg.content}
|
|
116
|
+
html={msg.html}
|
|
117
|
+
streaming={msg.streaming}
|
|
118
|
+
status={msg.status}
|
|
119
|
+
allowCopy={allowCopy && partyOf(msg.role) === 'responder'}
|
|
120
|
+
attachments={typeof messageAttachments === 'function' ? attachmentsFor : null}
|
|
121
|
+
onretry={retryFor(msg)}
|
|
122
|
+
onfeedback={feedbackFor(msg)}
|
|
123
|
+
/>
|
|
124
|
+
{/if}
|
|
125
|
+
{/each}
|
|
126
|
+
|
|
127
|
+
{#if showJump}
|
|
128
|
+
<div class="jump">
|
|
129
|
+
<Button onclick={scrollToBottom} ariaLabel={jumpLabel}>
|
|
130
|
+
{#if typeof jumpIcon === 'function'}
|
|
131
|
+
{@render jumpIcon()}
|
|
132
|
+
{:else}
|
|
133
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
134
|
+
{@html chevronDownSvg}
|
|
135
|
+
{/if}
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<style>
|
|
142
|
+
.chat-message-list {
|
|
143
|
+
box-sizing: border-box;
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
gap: var(--chat-message-list-gap, 1rem);
|
|
147
|
+
flex: 1;
|
|
148
|
+
width: 100%;
|
|
149
|
+
overflow-y: auto;
|
|
150
|
+
padding: var(--chat-message-list-padding, 0.75rem 1.5rem);
|
|
151
|
+
scroll-behavior: var(--chat-message-list-scroll-behavior, smooth);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.jump {
|
|
155
|
+
position: sticky;
|
|
156
|
+
bottom: var(--chat-message-list-jump-bottom, 8px);
|
|
157
|
+
align-self: center;
|
|
158
|
+
margin-top: var(--chat-message-list-jump-margin-top, 4px);
|
|
159
|
+
--button-width: var(--chat-message-list-jump-size, 36px);
|
|
160
|
+
--button-height: var(--chat-message-list-jump-size, 36px);
|
|
161
|
+
--button-padding: var(--chat-message-list-jump-padding, 8px);
|
|
162
|
+
--button-border-radius: var(--chat-message-list-jump-border-radius, 50%);
|
|
163
|
+
--button-color: var(--chat-message-list-jump-background-color, #ffffff);
|
|
164
|
+
--button-text-color: var(--chat-message-list-jump-color, #52525b);
|
|
165
|
+
--button-border: var(--chat-message-list-jump-border, 1px solid #e4e4e7);
|
|
166
|
+
--button-box-shadow: var(--chat-message-list-jump-box-shadow, 0 4px 12px rgba(0, 0, 0, 0.12));
|
|
167
|
+
--button-content-gap: 0px;
|
|
168
|
+
--button-hover-color: var(--chat-message-list-jump-hover-background-color, #f4f4f5);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.jump :global(svg) {
|
|
172
|
+
height: 100%;
|
|
173
|
+
width: 100%;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@media (prefers-reduced-motion: reduce) {
|
|
177
|
+
.chat-message-list {
|
|
178
|
+
scroll-behavior: auto;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ChatMessageData } from '../Chat/types';
|
|
3
|
+
import type { ChatMessageFeedback } from '../ChatMessage/properties';
|
|
4
|
+
export type ChatMessageListProperties = OptionalChatMessageListProperties & ChatMessageListEventProperties & MandatoryChatMessageListProperties;
|
|
5
|
+
export type MandatoryChatMessageListProperties = {
|
|
6
|
+
messages: ChatMessageData[];
|
|
7
|
+
};
|
|
8
|
+
export type OptionalChatMessageListProperties = {
|
|
9
|
+
autoscroll?: boolean;
|
|
10
|
+
message?: Snippet<[ChatMessageData]>;
|
|
11
|
+
messageAttachments?: Snippet<[ChatMessageData]>;
|
|
12
|
+
empty?: Snippet;
|
|
13
|
+
jumpLabel?: string;
|
|
14
|
+
jumpIcon?: Snippet;
|
|
15
|
+
allowCopy?: boolean;
|
|
16
|
+
testId?: string;
|
|
17
|
+
classes?: string;
|
|
18
|
+
};
|
|
19
|
+
export type ChatMessageListEventProperties = {
|
|
20
|
+
onretry?: () => void;
|
|
21
|
+
onfeedback?: (value: ChatMessageFeedback, message: ChatMessageData) => void;
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Pill from '../Pill/Pill.svelte';
|
|
3
|
+
import type { ChatSuggestion, ChatSuggestionsProperties } from './properties';
|
|
4
|
+
|
|
5
|
+
let { items, disabled = false, onselect, testId, classes }: ChatSuggestionsProperties = $props();
|
|
6
|
+
|
|
7
|
+
function labelOf(item: ChatSuggestion): string {
|
|
8
|
+
return typeof item === 'string' ? item : item.label;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function valueOf(item: ChatSuggestion): string {
|
|
12
|
+
if (typeof item === 'string') {
|
|
13
|
+
return item;
|
|
14
|
+
}
|
|
15
|
+
return item.value ?? item.label;
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div
|
|
20
|
+
class="chat-suggestions {classes ?? ''}"
|
|
21
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
22
|
+
testID={typeof testId === 'string' ? testId : null}
|
|
23
|
+
>
|
|
24
|
+
{#each items as item, index (index)}
|
|
25
|
+
<div class="chip">
|
|
26
|
+
<Pill text={labelOf(item)} {disabled} onclick={() => onselect?.(valueOf(item), index)} />
|
|
27
|
+
</div>
|
|
28
|
+
{/each}
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
.chat-suggestions {
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-wrap: var(--chat-suggestions-flex-wrap, wrap);
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: var(--chat-suggestions-gap, 8px);
|
|
38
|
+
width: var(--chat-suggestions-width, 100%);
|
|
39
|
+
padding: var(--chat-suggestions-padding, 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.chip {
|
|
43
|
+
display: flex;
|
|
44
|
+
--pill-cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ChatSuggestion = string | {
|
|
2
|
+
label: string;
|
|
3
|
+
value?: string;
|
|
4
|
+
};
|
|
5
|
+
export type ChatSuggestionsProperties = OptionalChatSuggestionsProperties & ChatSuggestionsEventProperties & MandatoryChatSuggestionsProperties;
|
|
6
|
+
export type MandatoryChatSuggestionsProperties = {
|
|
7
|
+
items: ChatSuggestion[];
|
|
8
|
+
};
|
|
9
|
+
export type OptionalChatSuggestionsProperties = {
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
testId?: string;
|
|
12
|
+
classes?: string;
|
|
13
|
+
};
|
|
14
|
+
export type ChatSuggestionsEventProperties = {
|
|
15
|
+
onselect?: (value: string, index: number) => void;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|