@juspay/svelte-ui-components 2.125.0 → 2.127.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/AttachmentChipRow/AttachmentChipRow.svelte +354 -0
- package/dist/AttachmentChipRow/AttachmentChipRow.svelte.d.ts +4 -0
- package/dist/AttachmentChipRow/properties.d.ts +47 -0
- package/dist/AttachmentChipRow/properties.js +1 -0
- package/dist/ChatBubble/ChatBubble.svelte.d.ts +1 -1
- package/dist/ChatComposer/ChatComposer.svelte +96 -10
- package/dist/ChatComposer/properties.d.ts +76 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte +118 -11
- package/dist/ChatSuggestions/properties.d.ts +42 -0
- package/dist/HITL/HITL.svelte +576 -0
- package/dist/HITL/HITL.svelte.d.ts +4 -0
- package/dist/HITL/properties.d.ts +71 -0
- package/dist/HITL/properties.js +1 -0
- package/dist/HITL/timers.d.ts +6 -0
- package/dist/HITL/timers.js +7 -0
- package/dist/Pill/Pill.svelte +6 -0
- package/dist/Table/Table.svelte +109 -10
- package/dist/Table/properties.d.ts +1 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +217 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte.d.ts +4 -0
- package/dist/ThinkingIndicator/properties.d.ts +30 -0
- package/dist/ThinkingIndicator/properties.js +1 -0
- package/dist/Tooltip/tooltip-action.js +5 -0
- package/dist/TypewriterText/TypewriterText.svelte +110 -0
- package/dist/TypewriterText/TypewriterText.svelte.d.ts +4 -0
- package/dist/TypewriterText/properties.d.ts +21 -0
- package/dist/TypewriterText/properties.js +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { get } from 'svelte/store';
|
|
4
|
+
import Button from '../Button/Button.svelte';
|
|
5
|
+
import Card from '../Card/Card.svelte';
|
|
6
|
+
import Progress from '../Progress/Progress.svelte';
|
|
7
|
+
import { pauseAllConfirmationTimers } from './timers';
|
|
8
|
+
import type { HITLAction, HITLProperties, HITLResponse, HITLSection } from './properties';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Human-in-the-loop approval card: the assistant wants to run an action and the
|
|
12
|
+
* person approves or cancels it. Unless disabled, a countdown sweeps across the
|
|
13
|
+
* confirm button and auto-approves when it completes; interacting with any
|
|
14
|
+
* HITL pauses every sibling's countdown.
|
|
15
|
+
*/
|
|
16
|
+
let {
|
|
17
|
+
confirmationId,
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
sections,
|
|
21
|
+
functionArguments,
|
|
22
|
+
hiddenKeys,
|
|
23
|
+
onConfirm,
|
|
24
|
+
confirmLabel = 'Confirm',
|
|
25
|
+
cancelLabel = 'Cancel',
|
|
26
|
+
countdownSeconds = 10,
|
|
27
|
+
autoCancelSeconds = 0,
|
|
28
|
+
isMicMuted = false,
|
|
29
|
+
onMicToggle = null,
|
|
30
|
+
isHistoryMode = false,
|
|
31
|
+
initialState = null,
|
|
32
|
+
approvedIcon,
|
|
33
|
+
rejectedIcon,
|
|
34
|
+
badgeLabel = 'ACTION',
|
|
35
|
+
approvedLabel = 'Approved',
|
|
36
|
+
autoApprovedLabel = 'Completed',
|
|
37
|
+
rejectedLabel = 'Action halted',
|
|
38
|
+
expiredLabel = 'Action timed out',
|
|
39
|
+
testId,
|
|
40
|
+
confirmTestId,
|
|
41
|
+
cancelTestId,
|
|
42
|
+
completionTestId,
|
|
43
|
+
completionTextTestId,
|
|
44
|
+
classes
|
|
45
|
+
}: HITLProperties = $props();
|
|
46
|
+
|
|
47
|
+
const DEFAULT_HIDDEN_KEYS = [
|
|
48
|
+
'action',
|
|
49
|
+
'userhasconfirmed',
|
|
50
|
+
'confirmed',
|
|
51
|
+
'userid',
|
|
52
|
+
'sessionid',
|
|
53
|
+
'timestamp'
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
let isProcessing = $state(false);
|
|
57
|
+
let localResponse = $state<HITLResponse | null>(null);
|
|
58
|
+
let countdownActive = $state(false);
|
|
59
|
+
// Seeded from the prop; startCountdown re-seeds it on every (re)start.
|
|
60
|
+
// svelte-ignore state_referenced_locally
|
|
61
|
+
let timeRemaining = $state(countdownSeconds);
|
|
62
|
+
let countdownInterval: ReturnType<typeof setInterval> | null = null;
|
|
63
|
+
let autoCancelTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
64
|
+
let originalMicState: boolean | null = null;
|
|
65
|
+
|
|
66
|
+
// History mode renders the settled state from props; live mode from local state.
|
|
67
|
+
// A history card with no initialState renders as expired rather than falling
|
|
68
|
+
// through to the pending interactive card — a replayed card must never be able
|
|
69
|
+
// to fire onConfirm.
|
|
70
|
+
const userResponse = $derived.by((): HITLResponse | null => {
|
|
71
|
+
if (isHistoryMode) {
|
|
72
|
+
if (initialState === null) {
|
|
73
|
+
return 'expired';
|
|
74
|
+
}
|
|
75
|
+
if (initialState.status === 'EXPIRED') {
|
|
76
|
+
return 'expired';
|
|
77
|
+
}
|
|
78
|
+
return initialState.approved === true ? 'approved' : 'rejected';
|
|
79
|
+
}
|
|
80
|
+
return localResponse;
|
|
81
|
+
});
|
|
82
|
+
const isCompleted = $derived(userResponse !== null);
|
|
83
|
+
const isApproved = $derived(userResponse === 'approved' || userResponse === 'auto-approved');
|
|
84
|
+
const elapsedTime = $derived(countdownSeconds - timeRemaining);
|
|
85
|
+
|
|
86
|
+
const completionText = $derived(
|
|
87
|
+
userResponse === 'auto-approved'
|
|
88
|
+
? autoApprovedLabel
|
|
89
|
+
: userResponse === 'approved'
|
|
90
|
+
? approvedLabel
|
|
91
|
+
: userResponse === 'expired'
|
|
92
|
+
? expiredLabel
|
|
93
|
+
: rejectedLabel
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const stopCountdown = (): void => {
|
|
97
|
+
if (countdownInterval !== null) {
|
|
98
|
+
clearInterval(countdownInterval);
|
|
99
|
+
countdownInterval = null;
|
|
100
|
+
}
|
|
101
|
+
countdownActive = false;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const clearAutoCancel = (): void => {
|
|
105
|
+
if (autoCancelTimeout !== null) {
|
|
106
|
+
clearTimeout(autoCancelTimeout);
|
|
107
|
+
autoCancelTimeout = null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const restoreMicState = async (): Promise<void> => {
|
|
112
|
+
if (onMicToggle !== null && originalMicState !== null && isMicMuted !== originalMicState) {
|
|
113
|
+
try {
|
|
114
|
+
await onMicToggle();
|
|
115
|
+
} catch {
|
|
116
|
+
// Mic restoration is best-effort; a failure must not block the decision.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Latched by the first decision path (click, countdown, auto-cancel) BEFORE its
|
|
122
|
+
// await, so a second path cannot also run restoreMicState — two restorations
|
|
123
|
+
// toggle the mic back to the wrong state, since the isMicMuted prop cannot have
|
|
124
|
+
// updated in between.
|
|
125
|
+
let decisionPending = false;
|
|
126
|
+
|
|
127
|
+
const settle = (action: HITLAction): void => {
|
|
128
|
+
if (isProcessing || isCompleted) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
localResponse = action;
|
|
132
|
+
isProcessing = true;
|
|
133
|
+
try {
|
|
134
|
+
onConfirm?.({
|
|
135
|
+
confirmationId,
|
|
136
|
+
action,
|
|
137
|
+
approved: action !== 'rejected'
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
// The decision failed to hand off; release the card for another attempt.
|
|
141
|
+
localResponse = null;
|
|
142
|
+
decisionPending = false;
|
|
143
|
+
} finally {
|
|
144
|
+
isProcessing = false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const decide = async (action: HITLAction): Promise<void> => {
|
|
149
|
+
if (decisionPending || isCompleted) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
decisionPending = true;
|
|
153
|
+
await restoreMicState();
|
|
154
|
+
settle(action);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const startCountdown = (): void => {
|
|
158
|
+
countdownActive = true;
|
|
159
|
+
timeRemaining = countdownSeconds;
|
|
160
|
+
countdownInterval = setInterval(() => {
|
|
161
|
+
// A sibling card's interaction pauses this countdown on its next tick.
|
|
162
|
+
if (get(pauseAllConfirmationTimers)) {
|
|
163
|
+
stopCountdown();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
timeRemaining = timeRemaining - 0.1;
|
|
167
|
+
if (timeRemaining <= 0) {
|
|
168
|
+
stopCountdown();
|
|
169
|
+
void decide('auto-approved');
|
|
170
|
+
}
|
|
171
|
+
}, 100);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const interact = async (action: HITLAction): Promise<void> => {
|
|
175
|
+
pauseAllConfirmationTimers.set(true);
|
|
176
|
+
stopCountdown();
|
|
177
|
+
clearAutoCancel();
|
|
178
|
+
await decide(action);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
onMount(() => {
|
|
182
|
+
if (isHistoryMode) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (onMicToggle !== null) {
|
|
186
|
+
originalMicState = isMicMuted;
|
|
187
|
+
if (!isMicMuted) {
|
|
188
|
+
void Promise.resolve(onMicToggle()).catch(() => {
|
|
189
|
+
// Auto-mute is best-effort.
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
pauseAllConfirmationTimers.set(false);
|
|
194
|
+
if (countdownSeconds > 0) {
|
|
195
|
+
startCountdown();
|
|
196
|
+
} else if (autoCancelSeconds > 0) {
|
|
197
|
+
autoCancelTimeout = setTimeout(() => {
|
|
198
|
+
void decide('rejected');
|
|
199
|
+
}, autoCancelSeconds * 1000);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
onDestroy(() => {
|
|
204
|
+
stopCountdown();
|
|
205
|
+
clearAutoCancel();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const titleCase = (key: string): string => {
|
|
209
|
+
return key
|
|
210
|
+
.replace(/([A-Z])/g, ' $1')
|
|
211
|
+
.trim()
|
|
212
|
+
.toLowerCase()
|
|
213
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const formatValue = (value: unknown, indentLevel: number): string => {
|
|
217
|
+
const indent = ' '.repeat(indentLevel);
|
|
218
|
+
if (value === null) {
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
if (typeof value === 'string') {
|
|
222
|
+
const trimmed = value.trim();
|
|
223
|
+
if (!trimmed || trimmed === '-') {
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
if (trimmed === '*') {
|
|
227
|
+
return 'All';
|
|
228
|
+
}
|
|
229
|
+
return trimmed.replace(/\s+/g, ' ');
|
|
230
|
+
}
|
|
231
|
+
if (typeof value === 'number') {
|
|
232
|
+
return value.toString();
|
|
233
|
+
}
|
|
234
|
+
if (typeof value === 'boolean') {
|
|
235
|
+
return value ? 'Yes' : 'No';
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(value)) {
|
|
238
|
+
return value
|
|
239
|
+
.map((item) => {
|
|
240
|
+
if (typeof item === 'object' && item !== null) {
|
|
241
|
+
return formatValue(item, indentLevel + 1);
|
|
242
|
+
}
|
|
243
|
+
return `${indent}• ${formatValue(item, 0)}`;
|
|
244
|
+
})
|
|
245
|
+
.filter((line) => line)
|
|
246
|
+
.join('\n');
|
|
247
|
+
}
|
|
248
|
+
if (typeof value === 'object') {
|
|
249
|
+
const entries = Object.entries(value);
|
|
250
|
+
if (entries.length === 0) {
|
|
251
|
+
return '{}';
|
|
252
|
+
}
|
|
253
|
+
return entries
|
|
254
|
+
.map(([key, nested]) => {
|
|
255
|
+
const readableKey = titleCase(String(key));
|
|
256
|
+
if (typeof nested === 'object' && nested !== null && !Array.isArray(nested)) {
|
|
257
|
+
return `${indent}${readableKey}:\n${formatValue(nested, indentLevel + 1)}`;
|
|
258
|
+
}
|
|
259
|
+
const formatted = formatValue(nested, indentLevel + 1);
|
|
260
|
+
return formatted ? `${indent}${readableKey}: ${formatted}` : '';
|
|
261
|
+
})
|
|
262
|
+
.filter((line) => line)
|
|
263
|
+
.join('\n');
|
|
264
|
+
}
|
|
265
|
+
return JSON.stringify(value);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const formatArguments = (args: Record<string, unknown>): HITLSection[] => {
|
|
269
|
+
const hidden = (hiddenKeys ?? DEFAULT_HIDDEN_KEYS).map((key) => key.toLowerCase());
|
|
270
|
+
const built: HITLSection[] = [];
|
|
271
|
+
for (const [key, value] of Object.entries(args)) {
|
|
272
|
+
if (hidden.includes(key.toLowerCase()) || value === null) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const wildcardKey = key
|
|
276
|
+
.replace(/([A-Z])/g, ' $1')
|
|
277
|
+
.trim()
|
|
278
|
+
.replace(/\btype\b/gi, '')
|
|
279
|
+
.trim();
|
|
280
|
+
const formatted =
|
|
281
|
+
typeof value === 'string' && value.trim() === '*'
|
|
282
|
+
? `All ${wildcardKey}s`
|
|
283
|
+
: formatValue(value, 0);
|
|
284
|
+
if (formatted.trim()) {
|
|
285
|
+
built.push({ label: key.replace(/([A-Z])/g, ' $1').trim(), value: formatted });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return built.length > 0 ? built : [{ label: 'PARAMETERS', value: 'No parameters' }];
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const parameterSections = $derived.by((): HITLSection[] => {
|
|
292
|
+
if (sections && sections.length > 0) {
|
|
293
|
+
return sections;
|
|
294
|
+
}
|
|
295
|
+
if (functionArguments && Object.keys(functionArguments).length > 0) {
|
|
296
|
+
return formatArguments(functionArguments);
|
|
297
|
+
}
|
|
298
|
+
return [{ label: 'PARAMETERS', value: 'No parameters' }];
|
|
299
|
+
});
|
|
300
|
+
</script>
|
|
301
|
+
|
|
302
|
+
<div
|
|
303
|
+
class="hitl {classes ?? ''}"
|
|
304
|
+
data-confirmation-id={confirmationId}
|
|
305
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
306
|
+
testID={typeof testId === 'string' ? testId : null}
|
|
307
|
+
>
|
|
308
|
+
<Card
|
|
309
|
+
cssVars={{
|
|
310
|
+
'--card-background': 'var(--hitl-background, #ffffff)',
|
|
311
|
+
'--card-border': 'var(--hitl-border, 1px solid #e4e4e7)',
|
|
312
|
+
'--card-border-radius': 'var(--hitl-border-radius, 0.5rem)',
|
|
313
|
+
'--card-content-padding': 'var(--hitl-padding, 1.25rem)',
|
|
314
|
+
'--card-overflow': 'visible',
|
|
315
|
+
'--card-width': '100%'
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
<div class="confirmation-header">
|
|
319
|
+
<span class="badge">{badgeLabel}</span>
|
|
320
|
+
<span class="title" data-pw={testId && `${testId}-title`}>{title}</span>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div class="header-border"></div>
|
|
324
|
+
|
|
325
|
+
{#if typeof description === 'string' && description.length > 0}
|
|
326
|
+
<p class="description" data-pw={testId && `${testId}-description`}>{description}</p>
|
|
327
|
+
{/if}
|
|
328
|
+
|
|
329
|
+
<div class="confirmation-body">
|
|
330
|
+
{#each parameterSections as section, sectionIndex (sectionIndex)}
|
|
331
|
+
<div class="params">
|
|
332
|
+
<span class="parameter-label">{section.label}</span>
|
|
333
|
+
<span class="parameter-value">{section.value}</span>
|
|
334
|
+
</div>
|
|
335
|
+
{/each}
|
|
336
|
+
|
|
337
|
+
{#if isCompleted && userResponse !== null}
|
|
338
|
+
<div
|
|
339
|
+
class="completion"
|
|
340
|
+
data-pw={completionTestId ?? (testId && `${testId}-completion`) ?? null}
|
|
341
|
+
>
|
|
342
|
+
<span class="completion-icon" class:halted={!isApproved} aria-hidden="true">
|
|
343
|
+
{#if isApproved}
|
|
344
|
+
{#if approvedIcon}
|
|
345
|
+
{@render approvedIcon()}
|
|
346
|
+
{:else}
|
|
347
|
+
<svg viewBox="0 0 20 20" fill="none">
|
|
348
|
+
<circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.5" />
|
|
349
|
+
<path
|
|
350
|
+
d="M6 10.2l2.6 2.6L14 7.4"
|
|
351
|
+
stroke="currentColor"
|
|
352
|
+
stroke-width="1.5"
|
|
353
|
+
stroke-linecap="round"
|
|
354
|
+
stroke-linejoin="round"
|
|
355
|
+
/>
|
|
356
|
+
</svg>
|
|
357
|
+
{/if}
|
|
358
|
+
{:else if rejectedIcon}
|
|
359
|
+
{@render rejectedIcon()}
|
|
360
|
+
{:else}
|
|
361
|
+
<svg viewBox="0 0 20 20" fill="none">
|
|
362
|
+
<circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.5" />
|
|
363
|
+
<path d="M6 10h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
364
|
+
</svg>
|
|
365
|
+
{/if}
|
|
366
|
+
</span>
|
|
367
|
+
<span
|
|
368
|
+
class="completion-text"
|
|
369
|
+
class:halted={!isApproved}
|
|
370
|
+
data-pw={completionTextTestId ?? (testId && `${testId}-completion-text`) ?? null}
|
|
371
|
+
>
|
|
372
|
+
{completionText}
|
|
373
|
+
</span>
|
|
374
|
+
</div>
|
|
375
|
+
{:else}
|
|
376
|
+
<div class="action-buttons">
|
|
377
|
+
<div class="cancel-button">
|
|
378
|
+
<Button
|
|
379
|
+
variant="secondary"
|
|
380
|
+
text={cancelLabel}
|
|
381
|
+
enable={!isProcessing}
|
|
382
|
+
testId={cancelTestId ?? (testId && `${testId}-cancel`)}
|
|
383
|
+
onclick={() => interact('rejected')}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="confirm-button">
|
|
387
|
+
{#if countdownActive}
|
|
388
|
+
<div class="progress-anchor">
|
|
389
|
+
<Progress value={elapsedTime} max={countdownSeconds} />
|
|
390
|
+
</div>
|
|
391
|
+
{/if}
|
|
392
|
+
<Button
|
|
393
|
+
text={confirmLabel}
|
|
394
|
+
enable={!isProcessing}
|
|
395
|
+
testId={confirmTestId ?? (testId && `${testId}-confirm`)}
|
|
396
|
+
onclick={() => interact('approved')}
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
{/if}
|
|
401
|
+
</div>
|
|
402
|
+
</Card>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<style>
|
|
406
|
+
.hitl {
|
|
407
|
+
position: relative;
|
|
408
|
+
width: 100%;
|
|
409
|
+
max-width: var(--hitl-max-width, 100%);
|
|
410
|
+
margin: var(--hitl-margin, 0);
|
|
411
|
+
animation: hitl-slide-in 0.3s ease-out forwards;
|
|
412
|
+
contain: layout style;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.confirmation-header {
|
|
416
|
+
display: flex;
|
|
417
|
+
flex-direction: column;
|
|
418
|
+
gap: var(--hitl-header-gap, 2px);
|
|
419
|
+
margin-bottom: var(--hitl-header-margin-bottom, 0.75rem);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.badge {
|
|
423
|
+
font-size: var(--hitl-badge-font-size, 0.6875rem);
|
|
424
|
+
font-weight: var(--hitl-badge-font-weight, 600);
|
|
425
|
+
letter-spacing: var(--hitl-badge-letter-spacing, 0.06em);
|
|
426
|
+
color: var(--hitl-badge-color, #858585);
|
|
427
|
+
text-transform: uppercase;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.title {
|
|
431
|
+
font-size: var(--hitl-title-font-size, 1rem);
|
|
432
|
+
font-weight: var(--hitl-title-font-weight, 600);
|
|
433
|
+
color: var(--hitl-title-color, #1f1f23);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.header-border {
|
|
437
|
+
height: 1px;
|
|
438
|
+
background-color: var(--hitl-divider-color, #e4e4e7);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.description {
|
|
442
|
+
margin: 0;
|
|
443
|
+
padding: var(--hitl-description-padding, 0.25rem 0);
|
|
444
|
+
font-size: var(--hitl-description-font-size, 0.8125rem);
|
|
445
|
+
line-height: var(--hitl-description-line-height, inherit);
|
|
446
|
+
color: var(--hitl-description-color, #858585);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.confirmation-body {
|
|
450
|
+
display: flex;
|
|
451
|
+
flex-direction: column;
|
|
452
|
+
gap: var(--hitl-content-gap, 1rem);
|
|
453
|
+
margin-top: var(--hitl-content-margin-top, 0.75rem);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.params {
|
|
457
|
+
display: flex;
|
|
458
|
+
flex-direction: column;
|
|
459
|
+
gap: var(--hitl-param-gap, 0.25rem);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.parameter-label {
|
|
463
|
+
font-size: var(--hitl-param-label-font-size, 0.6875rem);
|
|
464
|
+
font-weight: var(--hitl-param-label-font-weight, 600);
|
|
465
|
+
letter-spacing: var(--hitl-param-label-letter-spacing, 0.04em);
|
|
466
|
+
color: var(--hitl-param-label-color, #858585);
|
|
467
|
+
word-spacing: var(--hitl-param-label-word-spacing, normal);
|
|
468
|
+
text-transform: uppercase;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.parameter-value {
|
|
472
|
+
white-space: pre-line;
|
|
473
|
+
overflow-wrap: break-word;
|
|
474
|
+
font-size: var(--hitl-param-value-font-size, 0.875rem);
|
|
475
|
+
line-height: var(--hitl-param-value-line-height, 1.4);
|
|
476
|
+
color: var(--hitl-param-value-color, #1f1f23);
|
|
477
|
+
word-spacing: var(--hitl-param-value-word-spacing, normal);
|
|
478
|
+
text-transform: capitalize;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.action-buttons {
|
|
482
|
+
display: flex;
|
|
483
|
+
gap: var(--hitl-buttons-gap, 0.75rem);
|
|
484
|
+
width: 100%;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.cancel-button,
|
|
488
|
+
.confirm-button {
|
|
489
|
+
flex: 1;
|
|
490
|
+
position: relative;
|
|
491
|
+
--button-width: 100%;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* The countdown sweep: a transparent Progress stretched over the confirm
|
|
495
|
+
button whose bar darkens what is underneath as it advances. */
|
|
496
|
+
.progress-anchor {
|
|
497
|
+
position: absolute;
|
|
498
|
+
inset: 0;
|
|
499
|
+
z-index: 1;
|
|
500
|
+
pointer-events: none;
|
|
501
|
+
border-radius: var(--hitl-border-radius, 0.5rem);
|
|
502
|
+
overflow: hidden;
|
|
503
|
+
--progress-track-background: transparent;
|
|
504
|
+
--progress-bar-background: transparent;
|
|
505
|
+
--progress-bar-transition: width 0.1s linear;
|
|
506
|
+
--progress-track-height: 100%;
|
|
507
|
+
--progress-container-padding: 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.progress-anchor > :global(*) {
|
|
511
|
+
height: 100%;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.progress-anchor :global(.bar) {
|
|
515
|
+
backdrop-filter: var(--hitl-countdown-filter, brightness(0.8));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.completion {
|
|
519
|
+
display: flex;
|
|
520
|
+
align-items: center;
|
|
521
|
+
justify-content: center;
|
|
522
|
+
gap: var(--hitl-completion-gap, 0.5rem);
|
|
523
|
+
background: var(--hitl-completion-background, #f4f4f5);
|
|
524
|
+
border-radius: var(--hitl-border-radius, 0.5rem);
|
|
525
|
+
padding: var(--hitl-completion-padding, 1rem);
|
|
526
|
+
animation: hitl-fade-in 0.3s ease-in-out;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.completion-icon {
|
|
530
|
+
display: inline-flex;
|
|
531
|
+
width: var(--hitl-completion-icon-size, 1.25rem);
|
|
532
|
+
height: var(--hitl-completion-icon-size, 1.25rem);
|
|
533
|
+
color: var(--hitl-approved-color, #16a34a);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.completion-icon svg {
|
|
537
|
+
width: 100%;
|
|
538
|
+
height: 100%;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.completion-icon.halted {
|
|
542
|
+
color: var(--hitl-halted-color, #b45309);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.completion-text {
|
|
546
|
+
font-size: var(--hitl-completion-font-size, 0.875rem);
|
|
547
|
+
font-weight: var(--hitl-completion-font-weight, 600);
|
|
548
|
+
color: var(--hitl-approved-color, #16a34a);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.completion-text.halted {
|
|
552
|
+
color: var(--hitl-halted-color, #b45309);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
@keyframes hitl-slide-in {
|
|
556
|
+
from {
|
|
557
|
+
opacity: 0;
|
|
558
|
+
transform: translateY(8px);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
to {
|
|
562
|
+
opacity: 1;
|
|
563
|
+
transform: translateY(0);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
@keyframes hitl-fade-in {
|
|
568
|
+
from {
|
|
569
|
+
opacity: 0;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
to {
|
|
573
|
+
opacity: 1;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type HITLAction = 'approved' | 'rejected' | 'auto-approved';
|
|
3
|
+
export type HITLResponse = HITLAction | 'expired';
|
|
4
|
+
export type HITLSection = {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
8
|
+
export type HITLEvent = {
|
|
9
|
+
confirmationId: string;
|
|
10
|
+
action: HITLAction;
|
|
11
|
+
approved: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type HITLInitialState = {
|
|
14
|
+
approved?: boolean;
|
|
15
|
+
/** `'EXPIRED'` renders the timed-out completion state. */
|
|
16
|
+
status?: string;
|
|
17
|
+
};
|
|
18
|
+
export type HITLProperties = OptionalHITLProperties & MandatoryHITLProperties;
|
|
19
|
+
export type MandatoryHITLProperties = {
|
|
20
|
+
confirmationId: string;
|
|
21
|
+
/** The action being approved, already humanised — e.g. "Create discount". */
|
|
22
|
+
title: string;
|
|
23
|
+
};
|
|
24
|
+
export type OptionalHITLProperties = {
|
|
25
|
+
description?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Labelled parameter blocks to show. When omitted, `functionArguments` is
|
|
28
|
+
* formatted generically instead.
|
|
29
|
+
*/
|
|
30
|
+
sections?: HITLSection[];
|
|
31
|
+
/** Raw arguments, formatted generically when no `sections` are given. */
|
|
32
|
+
functionArguments?: Record<string, unknown>;
|
|
33
|
+
/** Argument keys (case-insensitive) hidden from the generic formatting. */
|
|
34
|
+
hiddenKeys?: string[];
|
|
35
|
+
onConfirm?: (event: HITLEvent) => void;
|
|
36
|
+
confirmLabel?: string;
|
|
37
|
+
cancelLabel?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Seconds until the card auto-approves, with a sweep across the confirm
|
|
40
|
+
* button. `0` disables auto-approval.
|
|
41
|
+
*/
|
|
42
|
+
countdownSeconds?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Seconds until an untouched card auto-rejects — for confirmations that must
|
|
45
|
+
* not auto-approve (e.g. OAuth) but should not block a conversation forever.
|
|
46
|
+
*/
|
|
47
|
+
autoCancelSeconds?: number;
|
|
48
|
+
/** Current mic state — muted while the card is open, restored on decision. */
|
|
49
|
+
isMicMuted?: boolean;
|
|
50
|
+
onMicToggle?: (() => void | Promise<void>) | null;
|
|
51
|
+
/** Renders a settled card from `initialState` with no timers or buttons. */
|
|
52
|
+
isHistoryMode?: boolean;
|
|
53
|
+
initialState?: HITLInitialState | null;
|
|
54
|
+
approvedIcon?: Snippet;
|
|
55
|
+
rejectedIcon?: Snippet;
|
|
56
|
+
badgeLabel?: string;
|
|
57
|
+
approvedLabel?: string;
|
|
58
|
+
autoApprovedLabel?: string;
|
|
59
|
+
rejectedLabel?: string;
|
|
60
|
+
expiredLabel?: string;
|
|
61
|
+
testId?: string;
|
|
62
|
+
/** Override the confirm button's test id (default: `<testId>-confirm`). */
|
|
63
|
+
confirmTestId?: string;
|
|
64
|
+
/** Override the cancel button's test id (default: `<testId>-cancel`). */
|
|
65
|
+
cancelTestId?: string;
|
|
66
|
+
/** Override the completion strip's test id (default: `<testId>-completion`). */
|
|
67
|
+
completionTestId?: string;
|
|
68
|
+
/** Override the completion text's test id (default: `<testId>-completion-text`). */
|
|
69
|
+
completionTextTestId?: string;
|
|
70
|
+
classes?: string;
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared across every mounted HITL. When the user interacts with one
|
|
3
|
+
* card, all sibling cards stop their countdowns so an auto-approval never fires
|
|
4
|
+
* while the user is actively deciding.
|
|
5
|
+
*/
|
|
6
|
+
export declare const pauseAllConfirmationTimers: import("svelte/store").Writable<boolean>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Shared across every mounted HITL. When the user interacts with one
|
|
4
|
+
* card, all sibling cards stop their countdowns so an auto-approval never fires
|
|
5
|
+
* while the user is actively deciding.
|
|
6
|
+
*/
|
|
7
|
+
export const pauseAllConfirmationTimers = writable(false);
|
package/dist/Pill/Pill.svelte
CHANGED
|
@@ -81,6 +81,12 @@
|
|
|
81
81
|
.pill {
|
|
82
82
|
display: inline-flex;
|
|
83
83
|
align-items: center;
|
|
84
|
+
/* A pill is content-width by default. A caller stacking pills into a menu needs
|
|
85
|
+
them to fill the column and align their labels left, which is a layout choice
|
|
86
|
+
the call site owns — hence tokens rather than a variant. */
|
|
87
|
+
width: var(--pill-width, auto);
|
|
88
|
+
justify-content: var(--pill-justify-content, center);
|
|
89
|
+
text-align: var(--pill-text-align, center);
|
|
84
90
|
gap: var(--pill-gap, 4px);
|
|
85
91
|
background-color: var(--pill-background, #e0e0e0);
|
|
86
92
|
color: var(--pill-color, #333333);
|