@classic-homes/agent-chat 0.11.0 → 0.13.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/README.md +67 -2
- package/dist/components/AgentChat.svelte +60 -25
- package/dist/components/AgentChat.svelte.d.ts +11 -1
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/ArtifactPanel.svelte +4 -2
- package/dist/components/AssistantMessage.svelte +58 -16
- package/dist/components/AssistantMessage.svelte.d.ts +2 -0
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
- package/dist/components/ChatHeader.svelte +4 -1
- package/dist/components/ChatHeader.svelte.d.ts +2 -0
- package/dist/components/ChatHeader.svelte.d.ts.map +1 -1
- package/dist/components/ChatLauncher.svelte +175 -0
- package/dist/components/ChatLauncher.svelte.d.ts +26 -0
- package/dist/components/ChatLauncher.svelte.d.ts.map +1 -0
- package/dist/components/Composer.svelte +7 -3
- package/dist/components/Composer.svelte.d.ts.map +1 -1
- package/dist/components/FloatingWindow.svelte +280 -0
- package/dist/components/FloatingWindow.svelte.d.ts +35 -0
- package/dist/components/FloatingWindow.svelte.d.ts.map +1 -0
- package/dist/components/Markdown.svelte +4 -0
- package/dist/components/Markdown.svelte.d.ts.map +1 -1
- package/dist/components/MessageFeedback.svelte +11 -4
- package/dist/components/MessageFeedback.svelte.d.ts.map +1 -1
- package/dist/components/MessageFiles.svelte +133 -40
- package/dist/components/MessageFiles.svelte.d.ts +6 -0
- package/dist/components/MessageFiles.svelte.d.ts.map +1 -1
- package/dist/components/MessageItem.svelte +2 -0
- package/dist/components/MessageItem.svelte.d.ts +2 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +40 -7
- package/dist/components/MessageList.svelte.d.ts +3 -1
- package/dist/components/MessageList.svelte.d.ts.map +1 -1
- package/dist/components/ShortcutsDialog.svelte +10 -3
- package/dist/components/ShortcutsDialog.svelte.d.ts +2 -0
- package/dist/components/ShortcutsDialog.svelte.d.ts.map +1 -1
- package/dist/components/UiFrame.svelte +12 -4
- package/dist/components/UiFrame.svelte.d.ts.map +1 -1
- package/dist/components/UserMessage.svelte +10 -1
- package/dist/components/UserMessage.svelte.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +94 -61
- package/dist/reducer.d.ts +11 -0
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +18 -2
- package/dist/session.svelte.d.ts +40 -0
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +178 -23
- package/dist/shortcuts.d.ts +7 -0
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +9 -0
- package/dist/surface-geometry.d.ts +60 -0
- package/dist/surface-geometry.d.ts.map +1 -0
- package/dist/surface-geometry.js +112 -0
- package/dist/surface.svelte.d.ts +79 -0
- package/dist/surface.svelte.d.ts.map +1 -0
- package/dist/surface.svelte.js +178 -0
- package/dist/thinking.d.ts +10 -0
- package/dist/thinking.d.ts.map +1 -0
- package/dist/thinking.js +61 -0
- package/dist/ui-frame-protocol.d.ts +6 -0
- package/dist/ui-frame-protocol.d.ts.map +1 -1
- package/dist/ui-frame-protocol.js +11 -0
- package/package.json +8 -7
- package/dist/artifact-pages.test.d.ts +0 -2
- package/dist/artifact-pages.test.d.ts.map +0 -1
- package/dist/artifact-pages.test.js +0 -102
- package/dist/components/a11y.svelte.test.d.ts +0 -2
- package/dist/components/a11y.svelte.test.d.ts.map +0 -1
- package/dist/components/a11y.svelte.test.js +0 -183
- package/dist/components/agent-picker.svelte.test.d.ts +0 -2
- package/dist/components/agent-picker.svelte.test.d.ts.map +0 -1
- package/dist/components/agent-picker.svelte.test.js +0 -84
- package/dist/components/composer.svelte.test.d.ts +0 -2
- package/dist/components/composer.svelte.test.d.ts.map +0 -1
- package/dist/components/composer.svelte.test.js +0 -162
- package/dist/components/continue.svelte.test.d.ts +0 -2
- package/dist/components/continue.svelte.test.d.ts.map +0 -1
- package/dist/components/continue.svelte.test.js +0 -109
- package/dist/components/conversation-chrome.svelte.test.d.ts +0 -2
- package/dist/components/conversation-chrome.svelte.test.d.ts.map +0 -1
- package/dist/components/conversation-chrome.svelte.test.js +0 -147
- package/dist/components/feedback.svelte.test.d.ts +0 -2
- package/dist/components/feedback.svelte.test.d.ts.map +0 -1
- package/dist/components/feedback.svelte.test.js +0 -99
- package/dist/components/live-fixes.svelte.test.d.ts +0 -2
- package/dist/components/live-fixes.svelte.test.d.ts.map +0 -1
- package/dist/components/live-fixes.svelte.test.js +0 -127
- package/dist/components/markdown-prose.svelte.test.d.ts +0 -2
- package/dist/components/markdown-prose.svelte.test.d.ts.map +0 -1
- package/dist/components/markdown-prose.svelte.test.js +0 -26
- package/dist/components/messages.svelte.test.d.ts +0 -2
- package/dist/components/messages.svelte.test.d.ts.map +0 -1
- package/dist/components/messages.svelte.test.js +0 -215
- package/dist/components/regenerate.svelte.test.d.ts +0 -2
- package/dist/components/regenerate.svelte.test.d.ts.map +0 -1
- package/dist/components/regenerate.svelte.test.js +0 -108
- package/dist/components/welcome.svelte.test.d.ts +0 -2
- package/dist/components/welcome.svelte.test.d.ts.map +0 -1
- package/dist/components/welcome.svelte.test.js +0 -119
- package/dist/composer-keys.test.d.ts +0 -2
- package/dist/composer-keys.test.d.ts.map +0 -1
- package/dist/composer-keys.test.js +0 -16
- package/dist/markdown.test.d.ts +0 -2
- package/dist/markdown.test.d.ts.map +0 -1
- package/dist/markdown.test.js +0 -35
- package/dist/reducer.test.d.ts +0 -2
- package/dist/reducer.test.d.ts.map +0 -1
- package/dist/reducer.test.js +0 -153
- package/dist/ui-frame-protocol.test.d.ts +0 -2
- package/dist/ui-frame-protocol.test.d.ts.map +0 -1
- package/dist/ui-frame-protocol.test.js +0 -48
- package/dist/ui-frame-relay.test.d.ts +0 -2
- package/dist/ui-frame-relay.test.d.ts.map +0 -1
- package/dist/ui-frame-relay.test.js +0 -36
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The floating launcher: a round button that opens the chat. It starts in a viewport corner;
|
|
4
|
+
* the user can drag it anywhere and it stays where it is dropped (kept inside the viewport),
|
|
5
|
+
* independent of where the chat window sits. It also reports what the chat
|
|
6
|
+
* is doing while collapsed (answering, an unseen answer, an error) and keeps the mock backend
|
|
7
|
+
* visible (AGENTS.md: mock must always be visible).
|
|
8
|
+
*/
|
|
9
|
+
import { cn, Icon } from '@classic-homes/theme-svelte';
|
|
10
|
+
import type { LauncherStatus } from '../surface.svelte.js';
|
|
11
|
+
import { DRAG_THRESHOLD, clampLauncher, type Corner, type Point } from '../surface-geometry.js';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Where it sits until the user places it. */
|
|
15
|
+
corner?: Corner;
|
|
16
|
+
/** Where the user placed it (top-left, viewport px); null or absent uses `corner`. */
|
|
17
|
+
position?: Point | null;
|
|
18
|
+
status?: LauncherStatus;
|
|
19
|
+
/** The backend is the mock: show the marker on the button. */
|
|
20
|
+
mock?: boolean;
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Id of the surface this button opens, for `aria-controls`. */
|
|
23
|
+
controls?: string;
|
|
24
|
+
expanded?: boolean;
|
|
25
|
+
draggable?: boolean;
|
|
26
|
+
onclick?: () => void;
|
|
27
|
+
/** The user dropped the button here (top-left, already inside the viewport). */
|
|
28
|
+
onmove?: (position: Point) => void;
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
let {
|
|
32
|
+
corner = 'bottom-right',
|
|
33
|
+
position = null,
|
|
34
|
+
status = 'idle',
|
|
35
|
+
mock = false,
|
|
36
|
+
label = 'Classic Agent',
|
|
37
|
+
controls,
|
|
38
|
+
expanded = false,
|
|
39
|
+
draggable = true,
|
|
40
|
+
onclick,
|
|
41
|
+
onmove,
|
|
42
|
+
class: className,
|
|
43
|
+
}: Props = $props();
|
|
44
|
+
|
|
45
|
+
let button: HTMLButtonElement | undefined = $state();
|
|
46
|
+
export function focus(): void {
|
|
47
|
+
button?.focus();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const STATUS_TEXT: Record<LauncherStatus, string> = {
|
|
51
|
+
idle: '',
|
|
52
|
+
streaming: 'answering',
|
|
53
|
+
unread: 'new answer',
|
|
54
|
+
error: 'something went wrong',
|
|
55
|
+
};
|
|
56
|
+
const name = $derived(
|
|
57
|
+
[label, STATUS_TEXT[status], mock ? 'mock backend' : ''].filter(Boolean).join(', '),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const CORNER_CLASS: Record<Corner, string> = {
|
|
61
|
+
'bottom-right': 'bottom-5 right-5',
|
|
62
|
+
'bottom-left': 'bottom-5 left-5',
|
|
63
|
+
'top-right': 'top-5 right-5',
|
|
64
|
+
'top-left': 'top-5 left-5',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const viewport = () => ({ width: window.innerWidth, height: window.innerHeight });
|
|
68
|
+
// A placed button has to stay reachable when the window shrinks.
|
|
69
|
+
let size = $state(typeof window === 'undefined' ? { width: 1280, height: 800 } : viewport());
|
|
70
|
+
const placed = $derived(position ? clampLauncher(position, size) : null);
|
|
71
|
+
|
|
72
|
+
let start: Point | null = null;
|
|
73
|
+
let offset = $state<Point>({ x: 0, y: 0 });
|
|
74
|
+
let dragging = $state(false);
|
|
75
|
+
// A drag ends in a click event on the same button; that click must not open the chat.
|
|
76
|
+
let suppressClick = false;
|
|
77
|
+
|
|
78
|
+
function onpointerdown(event: PointerEvent): void {
|
|
79
|
+
if (!draggable || event.button !== 0) return;
|
|
80
|
+
start = { x: event.clientX, y: event.clientY };
|
|
81
|
+
button?.setPointerCapture?.(event.pointerId);
|
|
82
|
+
}
|
|
83
|
+
function onpointermove(event: PointerEvent): void {
|
|
84
|
+
if (!start) return;
|
|
85
|
+
const dx = event.clientX - start.x;
|
|
86
|
+
const dy = event.clientY - start.y;
|
|
87
|
+
if (!dragging && Math.abs(dx) <= DRAG_THRESHOLD && Math.abs(dy) <= DRAG_THRESHOLD) return;
|
|
88
|
+
dragging = true;
|
|
89
|
+
offset = { x: dx, y: dy };
|
|
90
|
+
}
|
|
91
|
+
function onpointerup(event: PointerEvent): void {
|
|
92
|
+
if (!start) return;
|
|
93
|
+
start = null;
|
|
94
|
+
button?.releasePointerCapture?.(event.pointerId);
|
|
95
|
+
if (!dragging) return;
|
|
96
|
+
dragging = false;
|
|
97
|
+
suppressClick = true;
|
|
98
|
+
// Where the button is now, drag offset included.
|
|
99
|
+
const box = button?.getBoundingClientRect();
|
|
100
|
+
offset = { x: 0, y: 0 };
|
|
101
|
+
if (box) onmove?.(clampLauncher({ x: box.left, y: box.top }, viewport()));
|
|
102
|
+
}
|
|
103
|
+
function onpointercancel(): void {
|
|
104
|
+
start = null;
|
|
105
|
+
dragging = false;
|
|
106
|
+
offset = { x: 0, y: 0 };
|
|
107
|
+
}
|
|
108
|
+
function handleClick(): void {
|
|
109
|
+
if (suppressClick) {
|
|
110
|
+
suppressClick = false;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
onclick?.();
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<svelte:window onresize={() => (size = viewport())} />
|
|
118
|
+
|
|
119
|
+
<button
|
|
120
|
+
bind:this={button}
|
|
121
|
+
type="button"
|
|
122
|
+
class={cn(
|
|
123
|
+
'fixed z-40 flex size-14 touch-none select-none items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg outline-none ring-offset-background transition-shadow hover:shadow-xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
124
|
+
!placed && CORNER_CLASS[corner],
|
|
125
|
+
dragging ? 'scale-105 cursor-grabbing opacity-85' : 'cursor-pointer',
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
128
|
+
style:left={placed ? `${placed.x}px` : undefined}
|
|
129
|
+
style:top={placed ? `${placed.y}px` : undefined}
|
|
130
|
+
style:transform={dragging ? `translate(${offset.x}px, ${offset.y}px)` : undefined}
|
|
131
|
+
aria-label={name}
|
|
132
|
+
aria-expanded={expanded}
|
|
133
|
+
aria-controls={controls}
|
|
134
|
+
data-agent-launcher
|
|
135
|
+
data-corner={corner}
|
|
136
|
+
data-status={status}
|
|
137
|
+
onclick={handleClick}
|
|
138
|
+
{onpointerdown}
|
|
139
|
+
{onpointermove}
|
|
140
|
+
{onpointerup}
|
|
141
|
+
{onpointercancel}
|
|
142
|
+
>
|
|
143
|
+
{#if status === 'streaming'}
|
|
144
|
+
<!-- Answering: an arc orbits just outside the button and the icon breathes. With reduced motion
|
|
145
|
+
the rim stays as a still ring. -->
|
|
146
|
+
<span
|
|
147
|
+
class="pointer-events-none absolute -inset-[7px] rounded-full animate-spin [animation-duration:1.1s] motion-reduce:animate-none"
|
|
148
|
+
style="background: conic-gradient(from 0deg, transparent 0 35%, hsl(var(--primary)) 100%); -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px)); mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));"
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
data-testid="launcher-ring"
|
|
151
|
+
></span>
|
|
152
|
+
{/if}
|
|
153
|
+
<span class={cn('flex', status === 'streaming' && 'motion-safe:animate-pulse')}>
|
|
154
|
+
<Icon name="sparkles" size={24} />
|
|
155
|
+
</span>
|
|
156
|
+
{#if status === 'unread' || status === 'error'}
|
|
157
|
+
<span
|
|
158
|
+
class={cn(
|
|
159
|
+
'pointer-events-none absolute right-0.5 top-0.5 size-3.5 rounded-full border-2 border-background',
|
|
160
|
+
status === 'error' ? 'bg-destructive' : 'bg-success',
|
|
161
|
+
)}
|
|
162
|
+
aria-hidden="true"
|
|
163
|
+
data-testid="launcher-dot"
|
|
164
|
+
></span>
|
|
165
|
+
{/if}
|
|
166
|
+
{#if mock}
|
|
167
|
+
<span
|
|
168
|
+
class="pointer-events-none absolute -bottom-1 left-1/2 -translate-x-1/2 rounded bg-warning px-1 text-[10px] font-bold leading-4 tracking-wide text-warning-foreground"
|
|
169
|
+
aria-hidden="true"
|
|
170
|
+
data-testid="launcher-mock"
|
|
171
|
+
>
|
|
172
|
+
MOCK
|
|
173
|
+
</span>
|
|
174
|
+
{/if}
|
|
175
|
+
</button>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LauncherStatus } from '../surface.svelte.js';
|
|
2
|
+
import { type Corner, type Point } from '../surface-geometry.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Where it sits until the user places it. */
|
|
5
|
+
corner?: Corner;
|
|
6
|
+
/** Where the user placed it (top-left, viewport px); null or absent uses `corner`. */
|
|
7
|
+
position?: Point | null;
|
|
8
|
+
status?: LauncherStatus;
|
|
9
|
+
/** The backend is the mock: show the marker on the button. */
|
|
10
|
+
mock?: boolean;
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Id of the surface this button opens, for `aria-controls`. */
|
|
13
|
+
controls?: string;
|
|
14
|
+
expanded?: boolean;
|
|
15
|
+
draggable?: boolean;
|
|
16
|
+
onclick?: () => void;
|
|
17
|
+
/** The user dropped the button here (top-left, already inside the viewport). */
|
|
18
|
+
onmove?: (position: Point) => void;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const ChatLauncher: import("svelte").Component<Props, {
|
|
22
|
+
focus: () => void;
|
|
23
|
+
}, "">;
|
|
24
|
+
type ChatLauncher = ReturnType<typeof ChatLauncher>;
|
|
25
|
+
export default ChatLauncher;
|
|
26
|
+
//# sourceMappingURL=ChatLauncher.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatLauncher.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatLauncher.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAiC,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAG9F,UAAU,KAAK;IACb,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,gFAAgF;IAChF,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,KAAK,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2HH,QAAA,MAAM,YAAY;iBArGG,IAAI;MAqGiC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -150,6 +150,8 @@
|
|
|
150
150
|
function onpaste(event: ClipboardEvent): void {
|
|
151
151
|
const pasted = [...(event.clipboardData?.files ?? [])];
|
|
152
152
|
if (!upload || pasted.length === 0) return;
|
|
153
|
+
// Excel and Word put both text and a picture of the cells on the clipboard: paste the text.
|
|
154
|
+
if (event.clipboardData?.getData?.('text/plain')) return;
|
|
153
155
|
// Pasting a screenshot or a copied file attaches it; plain text pastes as usual.
|
|
154
156
|
event.preventDefault();
|
|
155
157
|
void addFiles(pasted);
|
|
@@ -281,9 +283,11 @@
|
|
|
281
283
|
class={cn(
|
|
282
284
|
'text-xs tabular-nums text-muted-foreground',
|
|
283
285
|
chars >= LIMITS.messageChars && 'text-destructive',
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
)}>{chars.toLocaleString()} / {LIMITS.messageChars.toLocaleString()}</span
|
|
287
|
+
>
|
|
288
|
+
<!-- Announced once when the limit is crossed, not on every keystroke. -->
|
|
289
|
+
<span class="sr-only" aria-live="polite"
|
|
290
|
+
>{chars > LIMITS.messageChars ? 'Message is over the length limit' : ''}</span
|
|
287
291
|
>
|
|
288
292
|
{/if}
|
|
289
293
|
{#if streaming}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAIvF,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAIvF,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AAiQH;;;;GAIG;AACH,QAAA,MAAM,QAAQ;iBApOO,IAAI;qBAoO6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The detached chat window: a non-modal floating panel the user can drag by its header and
|
|
4
|
+
* resize from any edge, so the page under it stays usable. Geometry lives in the `ChatSurface`
|
|
5
|
+
* it is given. Keyboard: the move handle takes arrow keys (Shift resizes, Alt for small
|
|
6
|
+
* steps); Escape inside the window minimizes it unless something inside used the key.
|
|
7
|
+
*/
|
|
8
|
+
import { onMount, untrack, type Snippet } from 'svelte';
|
|
9
|
+
import { Button, cn, Icon } from '@classic-homes/theme-svelte';
|
|
10
|
+
import type { ChatSurface } from '../surface.svelte.js';
|
|
11
|
+
import {
|
|
12
|
+
DRAG_THRESHOLD,
|
|
13
|
+
KEY_STEP,
|
|
14
|
+
RESIZE_DIRECTIONS,
|
|
15
|
+
isDockDrop,
|
|
16
|
+
type Point,
|
|
17
|
+
type ResizeDirection,
|
|
18
|
+
} from '../surface-geometry.js';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
surface: ChatSurface;
|
|
22
|
+
title?: string;
|
|
23
|
+
/** A second, quieter line under the title, e.g. the conversation's name. */
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
/** Replaces the mark before the title. */
|
|
26
|
+
icon?: Snippet;
|
|
27
|
+
id?: string;
|
|
28
|
+
/** Header buttons before Dock and Minimize, e.g. New chat. */
|
|
29
|
+
actions?: Snippet;
|
|
30
|
+
children: Snippet;
|
|
31
|
+
/** Offer docking (button, and dropping the window on the right edge). */
|
|
32
|
+
dockable?: boolean;
|
|
33
|
+
resizable?: boolean;
|
|
34
|
+
/** Called after the window minimizes, e.g. to move focus to the launcher. */
|
|
35
|
+
onminimize?: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Take the keyboard when the window appears and nothing inside has it yet: the first text
|
|
38
|
+
* field, or the window itself. Without it Escape and Tab start from the page underneath.
|
|
39
|
+
*/
|
|
40
|
+
autofocus?: boolean;
|
|
41
|
+
class?: string;
|
|
42
|
+
}
|
|
43
|
+
let {
|
|
44
|
+
surface,
|
|
45
|
+
title = 'Classic Agent',
|
|
46
|
+
subtitle,
|
|
47
|
+
icon,
|
|
48
|
+
id = 'agent-chat-window',
|
|
49
|
+
actions,
|
|
50
|
+
children,
|
|
51
|
+
dockable = true,
|
|
52
|
+
resizable = true,
|
|
53
|
+
onminimize,
|
|
54
|
+
autofocus = true,
|
|
55
|
+
class: className,
|
|
56
|
+
}: Props = $props();
|
|
57
|
+
|
|
58
|
+
const rect = $derived(surface.rect);
|
|
59
|
+
// Untracked: attaching clamps the saved rect, which must not re-run this effect.
|
|
60
|
+
$effect(() => untrack(() => surface.attach()));
|
|
61
|
+
|
|
62
|
+
let root: HTMLElement | undefined = $state();
|
|
63
|
+
// After the content has mounted (and had its own chance to focus something).
|
|
64
|
+
onMount(() => {
|
|
65
|
+
if (!autofocus || !root || root.contains(document.activeElement)) return;
|
|
66
|
+
const field = root.querySelector<HTMLElement>(
|
|
67
|
+
'textarea:not([disabled]), input:not([disabled])',
|
|
68
|
+
);
|
|
69
|
+
(field ?? root).focus({ preventScroll: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function minimize(): void {
|
|
73
|
+
surface.minimize();
|
|
74
|
+
onminimize?.();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- drag by the header -------------------------------------------------------------------
|
|
78
|
+
let last: Point | null = null;
|
|
79
|
+
let dragged = false;
|
|
80
|
+
let dockHint = $state(false);
|
|
81
|
+
const viewport = () => ({ width: window.innerWidth, height: window.innerHeight });
|
|
82
|
+
|
|
83
|
+
function capture(event: PointerEvent): void {
|
|
84
|
+
(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);
|
|
85
|
+
}
|
|
86
|
+
function release(event: PointerEvent): void {
|
|
87
|
+
(event.currentTarget as HTMLElement).releasePointerCapture?.(event.pointerId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ondragdown(event: PointerEvent): void {
|
|
91
|
+
if (event.button !== 0) return;
|
|
92
|
+
// Buttons in the header keep their clicks.
|
|
93
|
+
if ((event.target as HTMLElement).closest('button, a, input, select, [data-no-drag]')) return;
|
|
94
|
+
last = { x: event.clientX, y: event.clientY };
|
|
95
|
+
dragged = false;
|
|
96
|
+
capture(event);
|
|
97
|
+
}
|
|
98
|
+
function ondragmove(event: PointerEvent): void {
|
|
99
|
+
if (!last) return;
|
|
100
|
+
const dx = event.clientX - last.x;
|
|
101
|
+
const dy = event.clientY - last.y;
|
|
102
|
+
if (!dragged && Math.abs(dx) <= DRAG_THRESHOLD && Math.abs(dy) <= DRAG_THRESHOLD) return;
|
|
103
|
+
dragged = true;
|
|
104
|
+
surface.moving = true;
|
|
105
|
+
last = { x: event.clientX, y: event.clientY };
|
|
106
|
+
surface.moveBy(dx, dy);
|
|
107
|
+
dockHint = dockable && isDockDrop(last, viewport());
|
|
108
|
+
}
|
|
109
|
+
function ondragup(event: PointerEvent): void {
|
|
110
|
+
if (!last) return;
|
|
111
|
+
const pointer = { x: event.clientX, y: event.clientY };
|
|
112
|
+
last = null;
|
|
113
|
+
dockHint = false;
|
|
114
|
+
release(event);
|
|
115
|
+
if (!dragged) return;
|
|
116
|
+
surface.commit();
|
|
117
|
+
if (dockable && isDockDrop(pointer, viewport())) surface.dock();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- resize from the edges ----------------------------------------------------------------
|
|
121
|
+
let resizing: { direction: ResizeDirection; last: Point } | null = null;
|
|
122
|
+
function onresizedown(event: PointerEvent, direction: ResizeDirection): void {
|
|
123
|
+
if (event.button !== 0) return;
|
|
124
|
+
resizing = { direction, last: { x: event.clientX, y: event.clientY } };
|
|
125
|
+
surface.moving = true;
|
|
126
|
+
capture(event);
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
function onresizemove(event: PointerEvent): void {
|
|
130
|
+
if (!resizing) return;
|
|
131
|
+
const dx = event.clientX - resizing.last.x;
|
|
132
|
+
const dy = event.clientY - resizing.last.y;
|
|
133
|
+
resizing.last = { x: event.clientX, y: event.clientY };
|
|
134
|
+
surface.resizeBy(resizing.direction, dx, dy);
|
|
135
|
+
}
|
|
136
|
+
function onresizeup(event: PointerEvent): void {
|
|
137
|
+
if (!resizing) return;
|
|
138
|
+
resizing = null;
|
|
139
|
+
release(event);
|
|
140
|
+
surface.commit();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const HANDLE_CLASS: Record<ResizeDirection, string> = {
|
|
144
|
+
n: 'inset-x-3 top-0 h-1.5 cursor-ns-resize',
|
|
145
|
+
s: 'inset-x-3 bottom-0 h-1.5 cursor-ns-resize',
|
|
146
|
+
e: 'inset-y-3 right-0 w-1.5 cursor-ew-resize',
|
|
147
|
+
w: 'inset-y-3 left-0 w-1.5 cursor-ew-resize',
|
|
148
|
+
ne: 'right-0 top-0 size-3 cursor-nesw-resize',
|
|
149
|
+
nw: 'left-0 top-0 size-3 cursor-nwse-resize',
|
|
150
|
+
se: 'bottom-0 right-0 size-3 cursor-nwse-resize',
|
|
151
|
+
sw: 'bottom-0 left-0 size-3 cursor-nesw-resize',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// --- keyboard ------------------------------------------------------------------------------
|
|
155
|
+
const ARROWS: Record<string, Point> = {
|
|
156
|
+
ArrowLeft: { x: -1, y: 0 },
|
|
157
|
+
ArrowRight: { x: 1, y: 0 },
|
|
158
|
+
ArrowUp: { x: 0, y: -1 },
|
|
159
|
+
ArrowDown: { x: 0, y: 1 },
|
|
160
|
+
};
|
|
161
|
+
function onhandlekeydown(event: KeyboardEvent): void {
|
|
162
|
+
const arrow = ARROWS[event.key];
|
|
163
|
+
if (!arrow) return;
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
const step = event.altKey ? 4 : KEY_STEP;
|
|
166
|
+
if (event.shiftKey) {
|
|
167
|
+
if (!resizable) return;
|
|
168
|
+
surface.resizeBy('se', arrow.x * step, arrow.y * step);
|
|
169
|
+
} else surface.moveBy(arrow.x * step, arrow.y * step);
|
|
170
|
+
surface.commit();
|
|
171
|
+
}
|
|
172
|
+
function onkeydown(event: KeyboardEvent): void {
|
|
173
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
174
|
+
// A menu, popover or dialog opened from inside the window closes first.
|
|
175
|
+
if (
|
|
176
|
+
document.querySelector(
|
|
177
|
+
'[data-state="open"]:is([role="menu"], [role="listbox"], [role="dialog"], [role="alertdialog"])',
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
return;
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
minimize();
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions, a11y_no_noninteractive_element_to_interactive_role -->
|
|
187
|
+
<section
|
|
188
|
+
bind:this={root}
|
|
189
|
+
{id}
|
|
190
|
+
tabindex="-1"
|
|
191
|
+
role="dialog"
|
|
192
|
+
aria-modal="false"
|
|
193
|
+
aria-labelledby="{id}-title"
|
|
194
|
+
class={cn(
|
|
195
|
+
'fixed z-40 flex flex-col overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-2xl outline-none',
|
|
196
|
+
surface.moving && 'select-none',
|
|
197
|
+
dockHint && 'ring-2 ring-primary',
|
|
198
|
+
className,
|
|
199
|
+
)}
|
|
200
|
+
style:left="{rect.x}px"
|
|
201
|
+
style:top="{rect.y}px"
|
|
202
|
+
style:width="{rect.width}px"
|
|
203
|
+
style:height="{rect.height}px"
|
|
204
|
+
data-agent-window
|
|
205
|
+
data-moving={surface.moving ? '' : undefined}
|
|
206
|
+
{onkeydown}
|
|
207
|
+
>
|
|
208
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
209
|
+
<div
|
|
210
|
+
class={cn(
|
|
211
|
+
'flex h-12 shrink-0 touch-none items-center gap-1 border-b border-border bg-muted/40 pl-1 pr-2',
|
|
212
|
+
surface.moving ? 'cursor-grabbing' : 'cursor-grab',
|
|
213
|
+
)}
|
|
214
|
+
data-testid="window-header"
|
|
215
|
+
onpointerdown={ondragdown}
|
|
216
|
+
onpointermove={ondragmove}
|
|
217
|
+
onpointerup={ondragup}
|
|
218
|
+
onpointercancel={ondragup}
|
|
219
|
+
>
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
class="flex size-8 shrink-0 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
223
|
+
aria-label={resizable
|
|
224
|
+
? 'Move window: arrow keys move, Shift and arrow keys resize'
|
|
225
|
+
: 'Move window: arrow keys move'}
|
|
226
|
+
data-no-drag
|
|
227
|
+
onkeydown={onhandlekeydown}
|
|
228
|
+
>
|
|
229
|
+
<Icon name="grip" size={14} />
|
|
230
|
+
</button>
|
|
231
|
+
{#if icon}
|
|
232
|
+
{@render icon()}
|
|
233
|
+
{:else}
|
|
234
|
+
<span
|
|
235
|
+
class="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary"
|
|
236
|
+
aria-hidden="true"
|
|
237
|
+
>
|
|
238
|
+
<Icon name="sparkles" size={14} />
|
|
239
|
+
</span>
|
|
240
|
+
{/if}
|
|
241
|
+
<div class="ml-1 flex min-w-0 flex-1 flex-col leading-tight">
|
|
242
|
+
<h2 id="{id}-title" class="truncate text-sm font-semibold">{title}</h2>
|
|
243
|
+
{#if subtitle}
|
|
244
|
+
<span class="truncate text-[11px] text-muted-foreground" data-testid="window-subtitle"
|
|
245
|
+
>{subtitle}</span
|
|
246
|
+
>
|
|
247
|
+
{/if}
|
|
248
|
+
</div>
|
|
249
|
+
{@render actions?.()}
|
|
250
|
+
{#if dockable}
|
|
251
|
+
<Button
|
|
252
|
+
variant="ghost"
|
|
253
|
+
size="sm"
|
|
254
|
+
aria-label="Dock to the side"
|
|
255
|
+
onclick={() => surface.dock()}
|
|
256
|
+
>
|
|
257
|
+
<Icon name="sidebar" size={14} />
|
|
258
|
+
</Button>
|
|
259
|
+
{/if}
|
|
260
|
+
<Button variant="ghost" size="sm" aria-label="Minimize" onclick={minimize}>
|
|
261
|
+
<Icon name="minus" size={14} />
|
|
262
|
+
</Button>
|
|
263
|
+
</div>
|
|
264
|
+
<div class={cn('min-h-0 flex-1', surface.moving && 'pointer-events-none')}>
|
|
265
|
+
{@render children()}
|
|
266
|
+
</div>
|
|
267
|
+
{#if resizable}
|
|
268
|
+
{#each RESIZE_DIRECTIONS as direction (direction)}
|
|
269
|
+
<div
|
|
270
|
+
class={cn('absolute touch-none', HANDLE_CLASS[direction])}
|
|
271
|
+
aria-hidden="true"
|
|
272
|
+
data-resize={direction}
|
|
273
|
+
onpointerdown={(event) => onresizedown(event, direction)}
|
|
274
|
+
onpointermove={onresizemove}
|
|
275
|
+
onpointerup={onresizeup}
|
|
276
|
+
onpointercancel={onresizeup}
|
|
277
|
+
></div>
|
|
278
|
+
{/each}
|
|
279
|
+
{/if}
|
|
280
|
+
</section>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detached chat window: a non-modal floating panel the user can drag by its header and
|
|
3
|
+
* resize from any edge, so the page under it stays usable. Geometry lives in the `ChatSurface`
|
|
4
|
+
* it is given. Keyboard: the move handle takes arrow keys (Shift resizes, Alt for small
|
|
5
|
+
* steps); Escape inside the window minimizes it unless something inside used the key.
|
|
6
|
+
*/
|
|
7
|
+
import { type Snippet } from 'svelte';
|
|
8
|
+
import type { ChatSurface } from '../surface.svelte.js';
|
|
9
|
+
interface Props {
|
|
10
|
+
surface: ChatSurface;
|
|
11
|
+
title?: string;
|
|
12
|
+
/** A second, quieter line under the title, e.g. the conversation's name. */
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
/** Replaces the mark before the title. */
|
|
15
|
+
icon?: Snippet;
|
|
16
|
+
id?: string;
|
|
17
|
+
/** Header buttons before Dock and Minimize, e.g. New chat. */
|
|
18
|
+
actions?: Snippet;
|
|
19
|
+
children: Snippet;
|
|
20
|
+
/** Offer docking (button, and dropping the window on the right edge). */
|
|
21
|
+
dockable?: boolean;
|
|
22
|
+
resizable?: boolean;
|
|
23
|
+
/** Called after the window minimizes, e.g. to move focus to the launcher. */
|
|
24
|
+
onminimize?: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Take the keyboard when the window appears and nothing inside has it yet: the first text
|
|
27
|
+
* field, or the window itself. Without it Escape and Tab start from the page underneath.
|
|
28
|
+
*/
|
|
29
|
+
autofocus?: boolean;
|
|
30
|
+
class?: string;
|
|
31
|
+
}
|
|
32
|
+
declare const FloatingWindow: import("svelte").Component<Props, {}, "">;
|
|
33
|
+
type FloatingWindow = ReturnType<typeof FloatingWindow>;
|
|
34
|
+
export default FloatingWindow;
|
|
35
|
+
//# sourceMappingURL=FloatingWindow.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FloatingWindow.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/FloatingWindow.svelte.ts"],"names":[],"mappings":"AAGA;;;;;KAKK;AACL,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWtD,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2MH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
copy.setAttribute('aria-label', lang ? `Copy ${lang} code` : 'Copy code');
|
|
71
71
|
copy.innerHTML = `${COPY_ICON}<span>Copy</span>`;
|
|
72
72
|
header.append(label, copy);
|
|
73
|
+
// Long lines scroll sideways: keyboard users need to reach (and name) the scroller.
|
|
74
|
+
pre.tabIndex = 0;
|
|
75
|
+
pre.setAttribute('role', 'region');
|
|
76
|
+
pre.setAttribute('aria-label', lang ? `${lang} code` : 'Code');
|
|
73
77
|
pre.replaceWith(wrap);
|
|
74
78
|
wrap.append(header, pre);
|
|
75
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Markdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Markdown.svelte.ts"],"names":[],"mappings":"AA2CE;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"Markdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Markdown.svelte.ts"],"names":[],"mappings":"AA2CE;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAyBnD;AAoBD,UAAU,KAAK;IACb,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAiCH;;;;;;;GAOG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -29,14 +29,21 @@
|
|
|
29
29
|
document.getElementById(noteId)?.focus();
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
let group = $state<HTMLSpanElement | null>(null);
|
|
33
|
+
/** Close the note and put the keyboard back on the thumbs-down it came from. */
|
|
34
|
+
async function closeNote(): Promise<void> {
|
|
35
|
+
asking = false;
|
|
36
|
+
await tick();
|
|
37
|
+
group?.querySelector<HTMLButtonElement>('button[aria-label="Bad answer"]')?.focus();
|
|
38
|
+
}
|
|
32
39
|
async function sendNote(): Promise<void> {
|
|
33
40
|
const text = note.trim();
|
|
34
|
-
|
|
41
|
+
void closeNote();
|
|
35
42
|
if (text) await onrate('down', text);
|
|
36
43
|
}
|
|
37
44
|
</script>
|
|
38
45
|
|
|
39
|
-
<span class="inline-flex items-center" role="group" aria-label="Rate this answer">
|
|
46
|
+
<span bind:this={group} class="inline-flex items-center" role="group" aria-label="Rate this answer">
|
|
40
47
|
<Button
|
|
41
48
|
variant="ghost"
|
|
42
49
|
size="icon"
|
|
@@ -79,11 +86,11 @@
|
|
|
79
86
|
onkeydown={(e: KeyboardEvent) => {
|
|
80
87
|
if (e.key === 'Escape') {
|
|
81
88
|
e.preventDefault();
|
|
82
|
-
|
|
89
|
+
void closeNote();
|
|
83
90
|
}
|
|
84
91
|
}}
|
|
85
92
|
/>
|
|
86
93
|
<Button type="submit" size="sm" variant="outline" disabled={!note.trim()}>Send</Button>
|
|
87
|
-
<Button type="button" size="sm" variant="ghost" onclick={() => (
|
|
94
|
+
<Button type="button" size="sm" variant="ghost" onclick={() => void closeNote()}>Skip</Button>
|
|
88
95
|
</form>
|
|
89
96
|
{/if}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageFeedback.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageFeedback.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAK/D,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;
|
|
1
|
+
{"version":3,"file":"MessageFeedback.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageFeedback.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAK/D,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAgEH;;;GAGG;AACH,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|