@r2digisolutions/components 0.17.2 → 0.17.3
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type
|
|
2
|
+
import { tick, type Snippet } from 'svelte';
|
|
3
3
|
import IconButton from '../../atoms/IconButton/IconButton.svelte';
|
|
4
4
|
import {
|
|
5
5
|
Sparkles,
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
Volume2,
|
|
11
11
|
VolumeX,
|
|
12
12
|
LoaderCircle,
|
|
13
|
-
RotateCcw
|
|
13
|
+
RotateCcw,
|
|
14
|
+
Copy,
|
|
15
|
+
Check
|
|
14
16
|
} from '@lucide/svelte';
|
|
15
17
|
|
|
16
18
|
export interface AssistantMessage {
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
draft?: string;
|
|
31
33
|
loading?: boolean;
|
|
32
34
|
loadingLabel?: string;
|
|
33
|
-
/** When false,
|
|
35
|
+
/** When false, the spinner bubble is hidden (parent paints status in a message). */
|
|
34
36
|
showLoadingBubble?: boolean;
|
|
35
37
|
error?: string | null;
|
|
36
38
|
voiceEnabled?: boolean;
|
|
@@ -43,12 +45,15 @@
|
|
|
43
45
|
stopMicLabel?: string;
|
|
44
46
|
voiceOnLabel?: string;
|
|
45
47
|
voiceOffLabel?: string;
|
|
48
|
+
copyChatLabel?: string;
|
|
49
|
+
copiedChatLabel?: string;
|
|
46
50
|
class?: string;
|
|
47
51
|
result?: Snippet<[AssistantMessage]>;
|
|
48
52
|
footerExtra?: Snippet;
|
|
49
53
|
onsend?: () => void;
|
|
50
54
|
onclose?: () => void;
|
|
51
55
|
onnewchat?: () => void;
|
|
56
|
+
oncopychat?: () => void | Promise<void>;
|
|
52
57
|
onmicclick?: () => void;
|
|
53
58
|
onvoiceoutputchange?: (on: boolean) => void;
|
|
54
59
|
onsuggestion?: (text: string) => void;
|
|
@@ -77,21 +82,46 @@
|
|
|
77
82
|
stopMicLabel = 'Detener micrófono',
|
|
78
83
|
voiceOnLabel = 'Voz activada',
|
|
79
84
|
voiceOffLabel = 'Voz desactivada',
|
|
85
|
+
copyChatLabel = 'Copiar chat',
|
|
86
|
+
copiedChatLabel = 'Copiado',
|
|
80
87
|
class: className = '',
|
|
81
88
|
result,
|
|
82
89
|
footerExtra,
|
|
83
90
|
onsend,
|
|
84
91
|
onclose,
|
|
85
92
|
onnewchat,
|
|
93
|
+
oncopychat,
|
|
86
94
|
onmicclick,
|
|
87
95
|
onvoiceoutputchange,
|
|
88
96
|
onsuggestion
|
|
89
97
|
}: AssistantChatProps = $props();
|
|
90
98
|
|
|
91
99
|
let scrollEl = $state<HTMLDivElement | null>(null);
|
|
100
|
+
let inputEl: HTMLInputElement | null = null;
|
|
101
|
+
/** Restore the composer after send / SvelteKit form focus reset. */
|
|
102
|
+
let retainComposerFocus = false;
|
|
103
|
+
let copiedFlash = $state(false);
|
|
92
104
|
|
|
93
105
|
const isEmpty = $derived(messages.length === 0 && !loading);
|
|
94
106
|
const canSend = $derived(draft.trim().length > 0 && !loading);
|
|
107
|
+
const pendingAssistantId = $derived.by(() => {
|
|
108
|
+
if (!loading || messages.length === 0) return null;
|
|
109
|
+
const last = messages[messages.length - 1];
|
|
110
|
+
if (last?.role === 'assistant' && !last.content.trim()) return last.id;
|
|
111
|
+
return null;
|
|
112
|
+
});
|
|
113
|
+
const showTrailingLoader = $derived(loading && showLoadingBubble && !pendingAssistantId);
|
|
114
|
+
|
|
115
|
+
function focusComposer() {
|
|
116
|
+
inputEl?.focus({ preventScroll: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Parent can call this after client-side navigation (goto). */
|
|
120
|
+
export function focus() {
|
|
121
|
+
retainComposerFocus = true;
|
|
122
|
+
focusComposer();
|
|
123
|
+
void tick().then(focusComposer);
|
|
124
|
+
}
|
|
95
125
|
|
|
96
126
|
$effect(() => {
|
|
97
127
|
void messages.length;
|
|
@@ -104,14 +134,26 @@
|
|
|
104
134
|
return () => cancelAnimationFrame(raf);
|
|
105
135
|
});
|
|
106
136
|
|
|
137
|
+
$effect(() => {
|
|
138
|
+
if (!open || loading || !retainComposerFocus) return;
|
|
139
|
+
const id = requestAnimationFrame(() => {
|
|
140
|
+
if (open && retainComposerFocus) focusComposer();
|
|
141
|
+
if (!loading) retainComposerFocus = false;
|
|
142
|
+
});
|
|
143
|
+
return () => cancelAnimationFrame(id);
|
|
144
|
+
});
|
|
145
|
+
|
|
107
146
|
function handleClose() {
|
|
147
|
+
retainComposerFocus = false;
|
|
108
148
|
open = false;
|
|
109
149
|
onclose?.();
|
|
110
150
|
}
|
|
111
151
|
|
|
112
152
|
function handleSend() {
|
|
113
153
|
if (!canSend) return;
|
|
154
|
+
retainComposerFocus = true;
|
|
114
155
|
onsend?.();
|
|
156
|
+
void tick().then(focusComposer);
|
|
115
157
|
}
|
|
116
158
|
|
|
117
159
|
function handleKeydown(e: KeyboardEvent) {
|
|
@@ -130,6 +172,15 @@
|
|
|
130
172
|
voiceOutput = !voiceOutput;
|
|
131
173
|
onvoiceoutputchange?.(voiceOutput);
|
|
132
174
|
}
|
|
175
|
+
|
|
176
|
+
async function handleCopyChat() {
|
|
177
|
+
if (!oncopychat || messages.length === 0) return;
|
|
178
|
+
await oncopychat();
|
|
179
|
+
copiedFlash = true;
|
|
180
|
+
window.setTimeout(() => {
|
|
181
|
+
copiedFlash = false;
|
|
182
|
+
}, 1600);
|
|
183
|
+
}
|
|
133
184
|
</script>
|
|
134
185
|
|
|
135
186
|
<div
|
|
@@ -154,6 +205,19 @@
|
|
|
154
205
|
<p class="text-xs text-muted truncate">{statusLabel}</p>
|
|
155
206
|
{/if}
|
|
156
207
|
</div>
|
|
208
|
+
<IconButton
|
|
209
|
+
variant="ghost"
|
|
210
|
+
size="xs"
|
|
211
|
+
label={copiedFlash ? copiedChatLabel : copyChatLabel}
|
|
212
|
+
disabled={messages.length === 0 || !oncopychat}
|
|
213
|
+
onclick={() => void handleCopyChat()}
|
|
214
|
+
>
|
|
215
|
+
{#if copiedFlash}
|
|
216
|
+
<Check size={14} strokeWidth={2} />
|
|
217
|
+
{:else}
|
|
218
|
+
<Copy size={14} strokeWidth={2} />
|
|
219
|
+
{/if}
|
|
220
|
+
</IconButton>
|
|
157
221
|
<IconButton variant="ghost" size="xs" label={newChatLabel} onclick={() => onnewchat?.()}>
|
|
158
222
|
<RotateCcw size={14} strokeWidth={2} />
|
|
159
223
|
</IconButton>
|
|
@@ -193,7 +257,7 @@
|
|
|
193
257
|
{#if message.role === 'user'}
|
|
194
258
|
<div class="flex justify-end">
|
|
195
259
|
<div
|
|
196
|
-
class="rounded-2xl rounded-br-sm bg-brand-600 px-3.5 py-2.5 text-sm leading-relaxed text-white max-w-[80%]"
|
|
260
|
+
class="rounded-2xl rounded-br-sm bg-brand-600 px-3.5 py-2.5 text-sm leading-relaxed text-white max-w-[80%] whitespace-pre-wrap"
|
|
197
261
|
>
|
|
198
262
|
{message.content}
|
|
199
263
|
</div>
|
|
@@ -208,9 +272,16 @@
|
|
|
208
272
|
<Sparkles size={12} strokeWidth={2} />
|
|
209
273
|
</span>
|
|
210
274
|
<div
|
|
211
|
-
class="rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5 text-sm leading-relaxed text-primary max-w-[calc(100%-2rem)]"
|
|
275
|
+
class="rounded-2xl rounded-bl-sm bg-surface-overlay px-3.5 py-2.5 text-sm leading-relaxed text-primary max-w-[calc(100%-2rem)] whitespace-pre-wrap"
|
|
212
276
|
>
|
|
213
|
-
{message.
|
|
277
|
+
{#if message.id === pendingAssistantId}
|
|
278
|
+
<span class="gap-2 text-muted flex items-center">
|
|
279
|
+
<LoaderCircle size={14} strokeWidth={2} class="animate-spin shrink-0" />
|
|
280
|
+
<span class="text-xs">{loadingLabel}</span>
|
|
281
|
+
</span>
|
|
282
|
+
{:else}
|
|
283
|
+
{message.content}
|
|
284
|
+
{/if}
|
|
214
285
|
</div>
|
|
215
286
|
</div>
|
|
216
287
|
{#if result}
|
|
@@ -222,7 +293,7 @@
|
|
|
222
293
|
{/if}
|
|
223
294
|
{/each}
|
|
224
295
|
|
|
225
|
-
{#if
|
|
296
|
+
{#if showTrailingLoader}
|
|
226
297
|
<div class="gap-2 flex items-center">
|
|
227
298
|
<span
|
|
228
299
|
class="h-6 w-6 bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400 flex shrink-0 items-center justify-center rounded-full"
|
|
@@ -246,6 +317,17 @@
|
|
|
246
317
|
<footer class="border-border px-3 py-2.5 shrink-0 border-t">
|
|
247
318
|
<div class="gap-1.5 flex items-center">
|
|
248
319
|
<input
|
|
320
|
+
{@attach (node) => {
|
|
321
|
+
inputEl = node;
|
|
322
|
+
$effect(() => {
|
|
323
|
+
if (!open || loading || !retainComposerFocus) return;
|
|
324
|
+
node.focus();
|
|
325
|
+
retainComposerFocus = false;
|
|
326
|
+
});
|
|
327
|
+
return () => {
|
|
328
|
+
if (inputEl === node) inputEl = null;
|
|
329
|
+
};
|
|
330
|
+
}}
|
|
249
331
|
type="text"
|
|
250
332
|
bind:value={draft}
|
|
251
333
|
placeholder={listening ? statusLabel || placeholder : placeholder}
|
|
@@ -253,7 +335,6 @@
|
|
|
253
335
|
'h-9 min-w-0 rounded-xl bg-surface px-3 text-sm text-primary placeholder:text-muted focus:border-brand-400 focus:ring-brand-500/20 flex-1 border transition-shadow outline-none focus:ring-2',
|
|
254
336
|
listening ? 'border-red-400 ring-red-500/20 ring-2' : 'border-border'
|
|
255
337
|
]}
|
|
256
|
-
disabled={loading}
|
|
257
338
|
onkeydown={handleKeydown}
|
|
258
339
|
aria-label={placeholder}
|
|
259
340
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
2
|
export interface AssistantMessage {
|
|
3
3
|
id: string;
|
|
4
4
|
role: 'user' | 'assistant';
|
|
@@ -15,7 +15,7 @@ interface AssistantChatProps {
|
|
|
15
15
|
draft?: string;
|
|
16
16
|
loading?: boolean;
|
|
17
17
|
loadingLabel?: string;
|
|
18
|
-
/** When false,
|
|
18
|
+
/** When false, the spinner bubble is hidden (parent paints status in a message). */
|
|
19
19
|
showLoadingBubble?: boolean;
|
|
20
20
|
error?: string | null;
|
|
21
21
|
voiceEnabled?: boolean;
|
|
@@ -28,16 +28,21 @@ interface AssistantChatProps {
|
|
|
28
28
|
stopMicLabel?: string;
|
|
29
29
|
voiceOnLabel?: string;
|
|
30
30
|
voiceOffLabel?: string;
|
|
31
|
+
copyChatLabel?: string;
|
|
32
|
+
copiedChatLabel?: string;
|
|
31
33
|
class?: string;
|
|
32
34
|
result?: Snippet<[AssistantMessage]>;
|
|
33
35
|
footerExtra?: Snippet;
|
|
34
36
|
onsend?: () => void;
|
|
35
37
|
onclose?: () => void;
|
|
36
38
|
onnewchat?: () => void;
|
|
39
|
+
oncopychat?: () => void | Promise<void>;
|
|
37
40
|
onmicclick?: () => void;
|
|
38
41
|
onvoiceoutputchange?: (on: boolean) => void;
|
|
39
42
|
onsuggestion?: (text: string) => void;
|
|
40
43
|
}
|
|
41
|
-
declare const AssistantChat: import("svelte").Component<AssistantChatProps, {
|
|
44
|
+
declare const AssistantChat: import("svelte").Component<AssistantChatProps, {
|
|
45
|
+
focus: () => void;
|
|
46
|
+
}, "open" | "draft" | "voiceOutput">;
|
|
42
47
|
type AssistantChat = ReturnType<typeof AssistantChat>;
|
|
43
48
|
export default AssistantChat;
|