@classic-homes/agent-chat 0.11.0 → 0.12.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 +57 -2
- package/dist/components/AgentChat.svelte +58 -22
- 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 +14 -5
- 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 +157 -0
- package/dist/components/ChatLauncher.svelte.d.ts +23 -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 +239 -0
- package/dist/components/FloatingWindow.svelte.d.ts +26 -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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +38 -0
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +176 -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 +56 -0
- package/dist/surface-geometry.d.ts.map +1 -0
- package/dist/surface-geometry.js +99 -0
- package/dist/surface.svelte.d.ts +69 -0
- package/dist/surface.svelte.d.ts.map +1 -0
- package/dist/surface.svelte.js +156 -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,239 @@
|
|
|
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 { 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
|
+
id?: string;
|
|
24
|
+
/** Header buttons before Dock and Minimize, e.g. New chat. */
|
|
25
|
+
actions?: Snippet;
|
|
26
|
+
children: Snippet;
|
|
27
|
+
/** Offer docking (button, and dropping the window on the right edge). */
|
|
28
|
+
dockable?: boolean;
|
|
29
|
+
resizable?: boolean;
|
|
30
|
+
/** Called after the window minimizes, e.g. to move focus to the launcher. */
|
|
31
|
+
onminimize?: () => void;
|
|
32
|
+
class?: string;
|
|
33
|
+
}
|
|
34
|
+
let {
|
|
35
|
+
surface,
|
|
36
|
+
title = 'Classic Agent',
|
|
37
|
+
id = 'agent-chat-window',
|
|
38
|
+
actions,
|
|
39
|
+
children,
|
|
40
|
+
dockable = true,
|
|
41
|
+
resizable = true,
|
|
42
|
+
onminimize,
|
|
43
|
+
class: className,
|
|
44
|
+
}: Props = $props();
|
|
45
|
+
|
|
46
|
+
const rect = $derived(surface.rect);
|
|
47
|
+
// Untracked: attaching clamps the saved rect, which must not re-run this effect.
|
|
48
|
+
$effect(() => untrack(() => surface.attach()));
|
|
49
|
+
|
|
50
|
+
function minimize(): void {
|
|
51
|
+
surface.minimize();
|
|
52
|
+
onminimize?.();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- drag by the header -------------------------------------------------------------------
|
|
56
|
+
let last: Point | null = null;
|
|
57
|
+
let dragged = false;
|
|
58
|
+
let dockHint = $state(false);
|
|
59
|
+
const viewport = () => ({ width: window.innerWidth, height: window.innerHeight });
|
|
60
|
+
|
|
61
|
+
function capture(event: PointerEvent): void {
|
|
62
|
+
(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);
|
|
63
|
+
}
|
|
64
|
+
function release(event: PointerEvent): void {
|
|
65
|
+
(event.currentTarget as HTMLElement).releasePointerCapture?.(event.pointerId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ondragdown(event: PointerEvent): void {
|
|
69
|
+
if (event.button !== 0) return;
|
|
70
|
+
// Buttons in the header keep their clicks.
|
|
71
|
+
if ((event.target as HTMLElement).closest('button, a, input, select, [data-no-drag]')) return;
|
|
72
|
+
last = { x: event.clientX, y: event.clientY };
|
|
73
|
+
dragged = false;
|
|
74
|
+
capture(event);
|
|
75
|
+
}
|
|
76
|
+
function ondragmove(event: PointerEvent): void {
|
|
77
|
+
if (!last) return;
|
|
78
|
+
const dx = event.clientX - last.x;
|
|
79
|
+
const dy = event.clientY - last.y;
|
|
80
|
+
if (!dragged && Math.abs(dx) <= DRAG_THRESHOLD && Math.abs(dy) <= DRAG_THRESHOLD) return;
|
|
81
|
+
dragged = true;
|
|
82
|
+
surface.moving = true;
|
|
83
|
+
last = { x: event.clientX, y: event.clientY };
|
|
84
|
+
surface.moveBy(dx, dy);
|
|
85
|
+
dockHint = dockable && isDockDrop(last, viewport());
|
|
86
|
+
}
|
|
87
|
+
function ondragup(event: PointerEvent): void {
|
|
88
|
+
if (!last) return;
|
|
89
|
+
const pointer = { x: event.clientX, y: event.clientY };
|
|
90
|
+
last = null;
|
|
91
|
+
dockHint = false;
|
|
92
|
+
release(event);
|
|
93
|
+
if (!dragged) return;
|
|
94
|
+
surface.commit();
|
|
95
|
+
if (dockable && isDockDrop(pointer, viewport())) surface.dock();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- resize from the edges ----------------------------------------------------------------
|
|
99
|
+
let resizing: { direction: ResizeDirection; last: Point } | null = null;
|
|
100
|
+
function onresizedown(event: PointerEvent, direction: ResizeDirection): void {
|
|
101
|
+
if (event.button !== 0) return;
|
|
102
|
+
resizing = { direction, last: { x: event.clientX, y: event.clientY } };
|
|
103
|
+
surface.moving = true;
|
|
104
|
+
capture(event);
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
}
|
|
107
|
+
function onresizemove(event: PointerEvent): void {
|
|
108
|
+
if (!resizing) return;
|
|
109
|
+
const dx = event.clientX - resizing.last.x;
|
|
110
|
+
const dy = event.clientY - resizing.last.y;
|
|
111
|
+
resizing.last = { x: event.clientX, y: event.clientY };
|
|
112
|
+
surface.resizeBy(resizing.direction, dx, dy);
|
|
113
|
+
}
|
|
114
|
+
function onresizeup(event: PointerEvent): void {
|
|
115
|
+
if (!resizing) return;
|
|
116
|
+
resizing = null;
|
|
117
|
+
release(event);
|
|
118
|
+
surface.commit();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const HANDLE_CLASS: Record<ResizeDirection, string> = {
|
|
122
|
+
n: 'inset-x-3 top-0 h-1.5 cursor-ns-resize',
|
|
123
|
+
s: 'inset-x-3 bottom-0 h-1.5 cursor-ns-resize',
|
|
124
|
+
e: 'inset-y-3 right-0 w-1.5 cursor-ew-resize',
|
|
125
|
+
w: 'inset-y-3 left-0 w-1.5 cursor-ew-resize',
|
|
126
|
+
ne: 'right-0 top-0 size-3 cursor-nesw-resize',
|
|
127
|
+
nw: 'left-0 top-0 size-3 cursor-nwse-resize',
|
|
128
|
+
se: 'bottom-0 right-0 size-3 cursor-nwse-resize',
|
|
129
|
+
sw: 'bottom-0 left-0 size-3 cursor-nesw-resize',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// --- keyboard ------------------------------------------------------------------------------
|
|
133
|
+
const ARROWS: Record<string, Point> = {
|
|
134
|
+
ArrowLeft: { x: -1, y: 0 },
|
|
135
|
+
ArrowRight: { x: 1, y: 0 },
|
|
136
|
+
ArrowUp: { x: 0, y: -1 },
|
|
137
|
+
ArrowDown: { x: 0, y: 1 },
|
|
138
|
+
};
|
|
139
|
+
function onhandlekeydown(event: KeyboardEvent): void {
|
|
140
|
+
const arrow = ARROWS[event.key];
|
|
141
|
+
if (!arrow) return;
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
const step = event.altKey ? 4 : KEY_STEP;
|
|
144
|
+
if (event.shiftKey) {
|
|
145
|
+
if (!resizable) return;
|
|
146
|
+
surface.resizeBy('se', arrow.x * step, arrow.y * step);
|
|
147
|
+
} else surface.moveBy(arrow.x * step, arrow.y * step);
|
|
148
|
+
surface.commit();
|
|
149
|
+
}
|
|
150
|
+
function onkeydown(event: KeyboardEvent): void {
|
|
151
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
152
|
+
// A menu, popover or dialog opened from inside the window closes first.
|
|
153
|
+
if (
|
|
154
|
+
document.querySelector(
|
|
155
|
+
'[data-state="open"]:is([role="menu"], [role="listbox"], [role="dialog"], [role="alertdialog"])',
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
return;
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
minimize();
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
165
|
+
<section
|
|
166
|
+
{id}
|
|
167
|
+
role="dialog"
|
|
168
|
+
aria-modal="false"
|
|
169
|
+
aria-labelledby="{id}-title"
|
|
170
|
+
class={cn(
|
|
171
|
+
'fixed z-40 flex flex-col overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-2xl',
|
|
172
|
+
surface.moving && 'select-none',
|
|
173
|
+
dockHint && 'ring-2 ring-primary',
|
|
174
|
+
className,
|
|
175
|
+
)}
|
|
176
|
+
style:left="{rect.x}px"
|
|
177
|
+
style:top="{rect.y}px"
|
|
178
|
+
style:width="{rect.width}px"
|
|
179
|
+
style:height="{rect.height}px"
|
|
180
|
+
data-agent-window
|
|
181
|
+
data-moving={surface.moving ? '' : undefined}
|
|
182
|
+
{onkeydown}
|
|
183
|
+
>
|
|
184
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
185
|
+
<div
|
|
186
|
+
class={cn(
|
|
187
|
+
'flex h-11 shrink-0 touch-none items-center gap-1 border-b border-border bg-muted/40 pl-1 pr-2',
|
|
188
|
+
surface.moving ? 'cursor-grabbing' : 'cursor-grab',
|
|
189
|
+
)}
|
|
190
|
+
data-testid="window-header"
|
|
191
|
+
onpointerdown={ondragdown}
|
|
192
|
+
onpointermove={ondragmove}
|
|
193
|
+
onpointerup={ondragup}
|
|
194
|
+
onpointercancel={ondragup}
|
|
195
|
+
>
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
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"
|
|
199
|
+
aria-label={resizable
|
|
200
|
+
? 'Move window: arrow keys move, Shift and arrow keys resize'
|
|
201
|
+
: 'Move window: arrow keys move'}
|
|
202
|
+
data-no-drag
|
|
203
|
+
onkeydown={onhandlekeydown}
|
|
204
|
+
>
|
|
205
|
+
<Icon name="grip" size={14} />
|
|
206
|
+
</button>
|
|
207
|
+
<h2 id="{id}-title" class="min-w-0 flex-1 truncate text-sm font-semibold">{title}</h2>
|
|
208
|
+
{@render actions?.()}
|
|
209
|
+
{#if dockable}
|
|
210
|
+
<Button
|
|
211
|
+
variant="ghost"
|
|
212
|
+
size="sm"
|
|
213
|
+
aria-label="Dock to the side"
|
|
214
|
+
onclick={() => surface.dock()}
|
|
215
|
+
>
|
|
216
|
+
<Icon name="sidebar" size={14} />
|
|
217
|
+
</Button>
|
|
218
|
+
{/if}
|
|
219
|
+
<Button variant="ghost" size="sm" aria-label="Minimize" onclick={minimize}>
|
|
220
|
+
<Icon name="minus" size={14} />
|
|
221
|
+
</Button>
|
|
222
|
+
</div>
|
|
223
|
+
<div class={cn('min-h-0 flex-1', surface.moving && 'pointer-events-none')}>
|
|
224
|
+
{@render children()}
|
|
225
|
+
</div>
|
|
226
|
+
{#if resizable}
|
|
227
|
+
{#each RESIZE_DIRECTIONS as direction (direction)}
|
|
228
|
+
<div
|
|
229
|
+
class={cn('absolute touch-none', HANDLE_CLASS[direction])}
|
|
230
|
+
aria-hidden="true"
|
|
231
|
+
data-resize={direction}
|
|
232
|
+
onpointerdown={(event) => onresizedown(event, direction)}
|
|
233
|
+
onpointermove={onresizemove}
|
|
234
|
+
onpointerup={onresizeup}
|
|
235
|
+
onpointercancel={onresizeup}
|
|
236
|
+
></div>
|
|
237
|
+
{/each}
|
|
238
|
+
{/if}
|
|
239
|
+
</section>
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
id?: string;
|
|
13
|
+
/** Header buttons before Dock and Minimize, e.g. New chat. */
|
|
14
|
+
actions?: Snippet;
|
|
15
|
+
children: Snippet;
|
|
16
|
+
/** Offer docking (button, and dropping the window on the right edge). */
|
|
17
|
+
dockable?: boolean;
|
|
18
|
+
resizable?: boolean;
|
|
19
|
+
/** Called after the window minimizes, e.g. to move focus to the launcher. */
|
|
20
|
+
onminimize?: () => void;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
declare const FloatingWindow: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type FloatingWindow = ReturnType<typeof FloatingWindow>;
|
|
25
|
+
export default FloatingWindow;
|
|
26
|
+
//# 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,EAAW,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWtD,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,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,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkLH,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"}
|
|
@@ -2,55 +2,150 @@
|
|
|
2
2
|
@component
|
|
3
3
|
Files the agent produced in a turn: images as thumbnails that open in a lightbox, everything
|
|
4
4
|
else as a download card with a type icon and size.
|
|
5
|
+
|
|
6
|
+
The file endpoint needs the bearer, which `<img src>` and download links cannot send. With
|
|
7
|
+
`loadFile` (e.g. the client's `fetchFile`) files are fetched with it and shown from blob URLs;
|
|
8
|
+
without it they link to `fileUrl` (cookie-authenticated hosts).
|
|
5
9
|
-->
|
|
6
10
|
<script lang="ts">
|
|
7
11
|
import { cn, Icon, Lightbox } from '@classic-homes/theme-svelte';
|
|
12
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
8
13
|
import type { FileState } from '../reducer.js';
|
|
9
14
|
import { fileIcon, formatBytes } from '../message-format.js';
|
|
10
15
|
|
|
11
16
|
interface Props {
|
|
12
17
|
files: FileState[];
|
|
13
18
|
fileUrl: (fileId: string) => string;
|
|
19
|
+
/** Fetch a file with the host's credentials; its blob is shown and downloaded. */
|
|
20
|
+
loadFile?: (fileId: string) => Promise<Blob>;
|
|
14
21
|
class?: string;
|
|
15
22
|
}
|
|
16
|
-
let { files, fileUrl, class: className }: Props = $props();
|
|
23
|
+
let { files, fileUrl, loadFile, class: className }: Props = $props();
|
|
24
|
+
|
|
25
|
+
type Loaded = { state: 'loading' } | { state: 'ready'; url: string } | { state: 'error' };
|
|
26
|
+
const loaded = new SvelteMap<string, Loaded>();
|
|
27
|
+
let destroyed = false;
|
|
28
|
+
|
|
29
|
+
function load(fileId: string): Promise<string | undefined> {
|
|
30
|
+
const known = loaded.get(fileId);
|
|
31
|
+
if (known?.state === 'ready') return Promise.resolve(known.url);
|
|
32
|
+
if (!loadFile) return Promise.resolve(undefined);
|
|
33
|
+
loaded.set(fileId, { state: 'loading' });
|
|
34
|
+
return loadFile(fileId).then(
|
|
35
|
+
(blob) => {
|
|
36
|
+
if (destroyed) return undefined;
|
|
37
|
+
const url = URL.createObjectURL(blob);
|
|
38
|
+
loaded.set(fileId, { state: 'ready', url });
|
|
39
|
+
return url;
|
|
40
|
+
},
|
|
41
|
+
() => {
|
|
42
|
+
if (!destroyed) loaded.set(fileId, { state: 'error' });
|
|
43
|
+
return undefined;
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
}
|
|
17
47
|
|
|
18
48
|
const images = $derived(files.filter((f) => f.mimeType.startsWith('image/')));
|
|
19
49
|
const others = $derived(files.filter((f) => !f.mimeType.startsWith('image/')));
|
|
50
|
+
|
|
51
|
+
// Thumbnails load at once; other files when they are downloaded.
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (!loadFile) return;
|
|
54
|
+
for (const file of images) if (!loaded.has(file.fileId)) void load(file.fileId);
|
|
55
|
+
});
|
|
56
|
+
$effect(() => () => {
|
|
57
|
+
destroyed = true;
|
|
58
|
+
for (const entry of loaded.values())
|
|
59
|
+
if (entry.state === 'ready') URL.revokeObjectURL(entry.url);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Where an image shows from: its blob once loaded, nothing while loading, else `fileUrl`. */
|
|
63
|
+
function srcFor(fileId: string): string | undefined {
|
|
64
|
+
if (!loadFile) return fileUrl(fileId);
|
|
65
|
+
const entry = loaded.get(fileId);
|
|
66
|
+
return entry?.state === 'ready' ? entry.url : undefined;
|
|
67
|
+
}
|
|
20
68
|
const items = $derived(
|
|
21
|
-
images.
|
|
22
|
-
|
|
23
|
-
kind: 'image' as const,
|
|
24
|
-
|
|
25
|
-
alt: f.name,
|
|
26
|
-
title: f.name,
|
|
27
|
-
})),
|
|
69
|
+
images.flatMap((f) => {
|
|
70
|
+
const src = srcFor(f.fileId);
|
|
71
|
+
return src ? [{ id: f.fileId, kind: 'image' as const, src, alt: f.name, title: f.name }] : [];
|
|
72
|
+
}),
|
|
28
73
|
);
|
|
29
74
|
let open = $state(false);
|
|
30
75
|
let index = $state(0);
|
|
76
|
+
|
|
77
|
+
async function download(file: FileState): Promise<void> {
|
|
78
|
+
const url = await load(file.fileId);
|
|
79
|
+
if (!url) return;
|
|
80
|
+
const a = document.createElement('a');
|
|
81
|
+
a.href = url;
|
|
82
|
+
a.download = file.name;
|
|
83
|
+
a.click();
|
|
84
|
+
}
|
|
85
|
+
const CARD =
|
|
86
|
+
'flex max-w-xs items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 text-left text-sm shadow-sm transition hover:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
31
87
|
</script>
|
|
32
88
|
|
|
89
|
+
{#snippet card(file: FileState)}
|
|
90
|
+
<span
|
|
91
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"
|
|
92
|
+
>
|
|
93
|
+
<Icon name={fileIcon(file.mimeType)} size={18} />
|
|
94
|
+
</span>
|
|
95
|
+
<span class="min-w-0 flex-1">
|
|
96
|
+
<span class="block truncate font-medium text-foreground">{file.name}</span>
|
|
97
|
+
<span class="block text-xs text-muted-foreground">
|
|
98
|
+
{#if loaded.get(file.fileId)?.state === 'error'}
|
|
99
|
+
Could not download; try again
|
|
100
|
+
{:else if loaded.get(file.fileId)?.state === 'loading'}
|
|
101
|
+
Downloading…
|
|
102
|
+
{:else}
|
|
103
|
+
{formatBytes(file.bytes)}
|
|
104
|
+
{/if}
|
|
105
|
+
</span>
|
|
106
|
+
</span>
|
|
107
|
+
<Icon name="download" size={16} class="text-muted-foreground" />
|
|
108
|
+
{/snippet}
|
|
109
|
+
|
|
33
110
|
<div class={cn('flex flex-col gap-2', className)}>
|
|
34
111
|
{#if images.length}
|
|
35
112
|
<ul class="flex flex-wrap gap-2" aria-label="Images">
|
|
36
|
-
{#each images as file
|
|
113
|
+
{#each images as file (file.fileId)}
|
|
114
|
+
{@const src = srcFor(file.fileId)}
|
|
115
|
+
{@const state = loadFile ? loaded.get(file.fileId)?.state : 'ready'}
|
|
37
116
|
<li>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
loading="lazy"
|
|
52
|
-
|
|
53
|
-
|
|
117
|
+
{#if src}
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
class="block overflow-hidden rounded-lg border border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
121
|
+
aria-label={`View ${file.name}`}
|
|
122
|
+
onclick={() => {
|
|
123
|
+
index = Math.max(
|
|
124
|
+
0,
|
|
125
|
+
items.findIndex((item) => item.id === file.fileId),
|
|
126
|
+
);
|
|
127
|
+
open = true;
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<img class="block h-40 max-w-xs object-cover" {src} alt={file.name} loading="lazy" />
|
|
131
|
+
</button>
|
|
132
|
+
{:else if state === 'error'}
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
class="flex h-40 w-40 flex-col items-center justify-center gap-1 rounded-lg border border-border bg-muted p-2 text-center text-xs text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
136
|
+
aria-label={`Could not load ${file.name}; try again`}
|
|
137
|
+
onclick={() => void load(file.fileId)}
|
|
138
|
+
>
|
|
139
|
+
<Icon name="refresh" size={16} />
|
|
140
|
+
<span class="line-clamp-2 break-all">{file.name}</span>
|
|
141
|
+
</button>
|
|
142
|
+
{:else}
|
|
143
|
+
<div
|
|
144
|
+
class="h-40 w-40 animate-pulse rounded-lg border border-border bg-muted"
|
|
145
|
+
role="img"
|
|
146
|
+
aria-label={`Loading ${file.name}`}
|
|
147
|
+
></div>
|
|
148
|
+
{/if}
|
|
54
149
|
</li>
|
|
55
150
|
{/each}
|
|
56
151
|
</ul>
|
|
@@ -60,22 +155,20 @@
|
|
|
60
155
|
<ul class="flex flex-wrap gap-2" aria-label="Files">
|
|
61
156
|
{#each others as file (file.fileId)}
|
|
62
157
|
<li>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"
|
|
158
|
+
{#if loadFile}
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
class={CARD}
|
|
162
|
+
aria-label={`Download ${file.name}`}
|
|
163
|
+
onclick={() => void download(file)}
|
|
70
164
|
>
|
|
71
|
-
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
</
|
|
77
|
-
|
|
78
|
-
</a>
|
|
165
|
+
{@render card(file)}
|
|
166
|
+
</button>
|
|
167
|
+
{:else}
|
|
168
|
+
<a class={CARD} href={fileUrl(file.fileId)} download={file.name}>
|
|
169
|
+
{@render card(file)}
|
|
170
|
+
</a>
|
|
171
|
+
{/if}
|
|
79
172
|
</li>
|
|
80
173
|
{/each}
|
|
81
174
|
</ul>
|
|
@@ -2,11 +2,17 @@ import type { FileState } from '../reducer.js';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
files: FileState[];
|
|
4
4
|
fileUrl: (fileId: string) => string;
|
|
5
|
+
/** Fetch a file with the host's credentials; its blob is shown and downloaded. */
|
|
6
|
+
loadFile?: (fileId: string) => Promise<Blob>;
|
|
5
7
|
class?: string;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Files the agent produced in a turn: images as thumbnails that open in a lightbox, everything
|
|
9
11
|
* else as a download card with a type icon and size.
|
|
12
|
+
*
|
|
13
|
+
* The file endpoint needs the bearer, which `<img src>` and download links cannot send. With
|
|
14
|
+
* `loadFile` (e.g. the client's `fetchFile`) files are fetched with it and shown from blob URLs;
|
|
15
|
+
* without it they link to `fileUrl` (cookie-authenticated hosts).
|
|
10
16
|
*/
|
|
11
17
|
declare const MessageFiles: import("svelte").Component<Props, {}, "">;
|
|
12
18
|
type MessageFiles = ReturnType<typeof MessageFiles>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageFiles.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageFiles.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MessageFiles.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageFiles.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI7C,UAAU,KAAK;IACb,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,kFAAkF;IAClF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6IH;;;;;;;GAOG;AACH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
last?: boolean;
|
|
15
15
|
sandboxOrigin: string;
|
|
16
16
|
fileUrl: (fileId: string) => string;
|
|
17
|
+
/** Fetch a file with the host's credentials (blob URLs); without it files link to `fileUrl`. */
|
|
18
|
+
loadFile?: (fileId: string) => Promise<Blob>;
|
|
17
19
|
toolLabels?: Record<string, string>;
|
|
18
20
|
onnavigate?: (href: string) => void;
|
|
19
21
|
onopenartifact?: (artifactId: string) => void;
|
|
@@ -6,6 +6,8 @@ interface Props {
|
|
|
6
6
|
last?: boolean;
|
|
7
7
|
sandboxOrigin: string;
|
|
8
8
|
fileUrl: (fileId: string) => string;
|
|
9
|
+
/** Fetch a file with the host's credentials (blob URLs); without it files link to `fileUrl`. */
|
|
10
|
+
loadFile?: (fileId: string) => Promise<Blob>;
|
|
9
11
|
toolLabels?: Record<string, string>;
|
|
10
12
|
onnavigate?: (href: string) => void;
|
|
11
13
|
onopenartifact?: (artifactId: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAmBH,sEAAsE;AACtE,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,gGAAgG;IAChG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAmBH,sEAAsE;AACtE,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|